Overview

In this module, we will explore dictionary-based word count approaches text analysis measurement.

We will be working with U.S. House floor speeches spanning June 1998 through July 1999. More specifically, we will:

  1. investigate variations in sentiment as a function of political party
  2. explore sentiment dynamics in the context of President Bill Clinton’s impeachment.

Environment Preparation

# Load packages 
library(pacman)
p_load(readr, dplyr, tidyr, ggplot2, jtools, 
       knitr, reshape2, jsonlite, lubridate, sentimentr, magrittr)

Data Preparation

In the preprocessing module (../notebooks/tdta_preprocessing.Rmd), we saved a tidy format data.frame containing our corpus. We’ll use that as a starting point for our work in this module.


dat_tidy <- readRDS('../data/tdta_clean_house_data_tidy.RDS')

dat_tidy
NA

In this data.frame, words are stored in the cells of the column word. Accordingly, an entire document, identified by the unique document identifier doc_num, is stored across multiple rows. This data.frame also contains metadata like the name of the speaker associated with a given document, as well as the the speaker’s party, district, and State.

Train/Test Split

If you have enough data, there’s really never any reason to not separate your data into a training set and testing set. Of course, determing how much data is enough can be tricky, because you don’t want to create a situation where you are underpowered or unable to estimate a target parameter with sufficient precision.

However, treating documents as our units of observation, we have an $N = $ 35,959, which is probably large enough for splitting.


doc_ids <- unique(dat_tidy$doc_num) # Get document IDs

n_docs = .50 * length(doc_ids) # Calculate number of documents to sample

set.seed(5435) # set seed for reproducibility

doc_ids_test = sample(doc_ids, n_docs) # sample document IDs for test data

dat_tidy.train <- dat_tidy %>%
  filter(doc_num %!in% doc_ids_test) # Select documents for training

dat_tidy.test <- dat_tidy %>%
  filter(doc_num %in% doc_ids_test) # Select documents for test

Conducting Word counts

Introduction to dictionaries

To conduct word count analyses, you need a dictionary or lexicon that specifies the words associated with your target construct(s).

To start, we’ll work with the NRC sentiment dictionary, which is one of three sentiment dictionaries packaged with tidytext:

  1. AFINN
  2. bing
  3. nrc

To access the NRC dictionary, we’ll use the get_sentiments function to store the NRC dictionary in an object called nrc_sent.

nrc_sent <- get_sentiments('nrc')
nrc_sent

nrc_sent contains two columns, word, which contains the words in the dictionary, and sentiment, which specifies the sentiment label associated with the word.

nrc_sent %>%
  count(sentiment)

The NRC dictionary contains 10 sentiment categories and each of these categories have varying numbers of words associated with them.

Let’s take a glance at first words in each category by spreading our tidy data.frame:

nrc_sent %>%
  group_by(sentiment) %>% 
  mutate(temp_id = row_number()) %>% # Create a temporary ID to weight top_n by
  top_n(n = -50, wt=temp_id) %>%     # Get first 50 items in each group 
  mutate(temp_id = row_number()) %>% # Create a temporary unique ID for each word in each group
  ungroup() %>%
  pivot_wider(names_from = sentiment, values_from = word) %>% # Spread our data
  select(-temp_id)

Glancing at these words, it’s clear that words are repeated in some categories.


tidytext word count sentiment analysis

In principle, tidytext makes simple dictionary-based word count sentiment analysis quite simple.

To count the words we can just:

  1. Conduct an inner_join between our data and our sentiment dictionary
  2. Count the matches

We’ll also divide the number of matches for each sentiment domain by the total number of words in our corpus. This will tell us the proportion associated with each sentiment domain.


total_words = nrow(dat_tidy.train)

dat_tidy.train %>%
  inner_join(nrc_sent) %>%
  count(sentiment) %>%
  mutate(prop = n/total_words) %>%
  arrange(desc(prop))
Joining, by = "word"

Affective sentiment by Political Party

We can also subset our data in order to ask more specific questions. For instance, we can easily estimate sentiment proportions for Democrats and Republicans.


dat_tidy.train.sent <- dat_tidy.train %>%
  group_by(Party) %>% # Group by Party
  mutate(total_words = n()) %>% # Calculate the total words in each group
  ungroup() %>% 
  inner_join(nrc_sent) # Drop words that aren't in sentiment dictionary
Joining, by = "word"
  
dat_tidy.train.sent %>% count(total_words, Party, sentiment) %>% # Count the number of rows in each Party for each sentiment
  mutate(prop = n/total_words) %>% # Calculate the proportion
  arrange(desc(prop)) %>% # Arrange in descending order by proportion positive
  select(-n, -total_words) %>%
  pivot_wider(names_from='Party', values_from = 'prop')
NA
NA
NA

Overall, it looks like there is very little mean sentiment variation between Republicans and Democrats. However, we’ve collapsed across documents. To get a better idea of how expressions of affective sentiment vary across Parties, let’s visualize the distribution of sentiment in documents

 
dat_tidy.train.sent %>%
  filter(Party !='Independent') %>%
  count(total_words, Party, doc_num, sentiment) %>%
  mutate(prop = n/total_words) %>%
  ggplot(aes(y = prop, x = Party, color=Party)) + 
  geom_jitter(alpha=.25) + 
  facet_wrap(.~sentiment, ncol=5) + 
  scale_colour_manual(values = c("blue", "red")) +
  theme_apa() + 
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  ggtitle('Document-level proportions of sentiment words by party') + 
  xlab('Party') + 
  ylab('Proportion')

What if we look at the document Ns of sentiment words instead of proportions?

 
dat_tidy.train.sent %>%
  filter(Party !='Independent') %>%
  count(total_words, Party, doc_num, sentiment) %>%
  mutate(prop = n/total_words) %>%
  ggplot(aes(y = n, x = Party, color=Party)) + 
  geom_jitter(alpha=.25) + 
  facet_wrap(.~sentiment, ncol=5) + 
  scale_colour_manual(values = c("blue", "red")) +
  theme() + 
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  ggtitle('Document-level Ns of sentiment words by party') + 
  xlab('Party') + 
  ylab('N')


Dynamics in affective sentiment by political party

Clearly, there isn’t much marginal between group variation in affective sentiment in this dataset. However, maybe there are interesting effects operating at other levels!

Let’s examine temporal variation in affective sentiment between parties. To do this, we will take a similar approach, but instead of counting the sentiment in each document, we’ll count the sentiment on each observed day.

First, however, let’s glance at the distribution of documents across time. For reference, let’s also add vertical lines to indicate the dates on which Clinton’s impeachment was iniated and voted on.


sent_time <- dat_tidy.train.sent %>%
  filter(Party !='Independent') %>%
  distinct(doc_num, .keep_all = T) %>%
  count(date, Party) %>%
  ggplot(aes(y = n, x = date, color=Party)) + 
  geom_line(alpha=.5) + 
  facet_wrap(Party ~. , ncol=1) +
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  geom_point() +
  theme_apa() +
  ggtitle('N of documents across time by party') +
  ylab('N') + 
  xlab('Date')

ggplotly(sent_time)

NA
NA

Clearly, there is substantial variation in the number of documents (i.e. speeches given by individual speakers) across time.


Now, let’s plot sentiment across time by party.


dat_tidy.train.sent %>%
  filter(Party !='Independent') %>%
  count(total_words, Party, date, sentiment) %>%
  mutate(prop = n/total_words) %>%
  ggplot(aes(y = n, x = date, color=Party)) + 
  facet_wrap(sentiment~Party, ncol=4) + 
  scale_colour_manual(values = c("blue", "red")) +
  theme() + 
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  ggtitle('Document-level Ns of sentiment words by party') + 
  xlab('Party') + 
  ylab('N') + 
  geom_smooth(color='black') +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2) +
  geom_point(alpha=.25) 


dat_tidy.train.sent %>%
  filter(Party !='Independent') %>%
  count(total_words, Party, date, sentiment) %>%
  mutate(prop = n/total_words) %>%
  ggplot(aes(y = prop, x = date, color=Party)) + 
  facet_wrap(sentiment~Party, ncol=4) + 
  scale_colour_manual(values = c("blue", "red")) +
  theme() + 
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) + 
  ggtitle('Document-level proportions of sentiment words by party') + 
  xlab('Party') + 
  ylab('N') + 
  geom_smooth(color='black') +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2) +
  geom_point(alpha=.25) 


Hypothesis testing with word counts


dat_tidy.train.speaker <- dat_tidy.train %>%
  group_by(Party, speaker) %>%
  mutate(speaker_total_words = n()) %>%
  ungroup() %>%
  inner_join(nrc_sent) %>%
  count(Party, speaker, speaker_total_words, date, sentiment) %>%
  group_by(speaker, sentiment) %>%
  mutate(speaker_sent_means = mean(n), 
         speaker_sent_cntr = n - speaker_sent_means)
Joining, by = "word"
  

dat_tidy.train.speaker <- dat_tidy.train %>%
  filter(Party != 'Independent') %>%
  group_by(Party, speaker, date) %>%
  mutate(speaker_day_n = n()) %>%
  ungroup() %>%
  inner_join(nrc_sent) %>%
  count(Party, speaker, date, speaker_day_n, sentiment) %>%
  mutate(speaker_day_sent_prop = n/speaker_day_n)
Joining, by = "word"
# Create a date range from the min/max dates in our training data
date_grid <- tibble(date = seq(min(dat_tidy.train.speaker$date), 
                               max(dat_tidy.train.speaker$date), by='days')) %>%
  mutate(date_int = row_number(), # Associate each date with an integer 
         date_int_scaled = date_int/100)

dat_tidy.train.speaker <- dat_tidy.train.speaker %>%
  left_join(date_grid) %>%
  mutate(date_int_scaled = date_int/100, 
         impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date"
  

Negative sentiment


train.speaker.negative.m1 <- dat_tidy.train.speaker %>%
  filter(sentiment=='negative') %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1  | speaker) + (1 + Party | date_int), data=.)

summary(train.speaker.negative.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int)
   Data: .

REML criterion at convergence: -34881.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.7629 -0.6398 -0.1575  0.4602  8.0026 

Random effects:
 Groups   Name            Variance  Std.Dev.  Corr 
 speaker  (Intercept)     2.464e-05 0.0049634      
 date_int (Intercept)     2.916e-05 0.0054003      
          PartyRepublican 7.967e-07 0.0008926 -0.42
 Residual                 2.760e-04 0.0166138      
Number of obs: 6630, groups:  speaker, 638; date_int, 144

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    0.0307114  0.0006638  46.265
PartyRepublican               -0.0001991  0.0006508  -0.306
impeachment_1                  0.0039442  0.0058847   0.670
impeachment_2                  0.0218401  0.0063649   3.431
PartyRepublican:impeachment_1  0.0004287  0.0033720   0.127
PartyRepublican:impeachment_2 -0.0002016  0.0048421  -0.042

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.522                            
impechmnt_1 -0.081  0.026                     
impechmnt_2 -0.079  0.028  0.010              
PrtyRpbl:_1  0.045 -0.081 -0.365 -0.007       
PrtyRpbl:_2  0.036 -0.064 -0.005 -0.426  0.013


dat_tidy.train.speaker.pred_grid <- expand.grid(Party = unique(dat_tidy.train.speaker$Party), 
            speaker = 'new_speaker', 
            date_int = unique(dat_tidy.train.speaker$date_int))


dat_tidy.train.speaker.pred_grid <- dat_tidy.train.speaker.pred_grid %>%
  left_join(date_grid) %>%
  mutate(impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date_int"
train.speaker.negative.m1.pred <- dat_tidy.train.speaker.pred_grid %>%
  mutate(preds = predict(train.speaker.negative.m1, newdata=dat_tidy.train.speaker.pred_grid, allow.new.levels=T))
  


train.speaker.negative.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of negative language for an average speaker' ) +
  ylab('Speaker proportion negative language') + 
  xlab('Date')
Joining, by = c("date_int", "date", "date_int_scaled")

NA

Disgust



train.speaker.disgust.m1 <- dat_tidy.train.speaker %>%
  filter(sentiment=='disgust') %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1  | speaker) + (1 + Party | date_int), data=.)

summary(train.speaker.disgust.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int)
   Data: .

REML criterion at convergence: -37281.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.1327 -0.6168 -0.2363  0.3417 11.8113 

Random effects:
 Groups   Name            Variance  Std.Dev.  Corr
 speaker  (Intercept)     3.769e-06 0.0019413     
 date_int (Intercept)     4.166e-06 0.0020410     
          PartyRepublican 1.502e-07 0.0003876 0.15
 Residual                 5.562e-05 0.0074581     
Number of obs: 5428, groups:  speaker, 576; date_int, 141

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    9.484e-03  2.782e-04  34.086
PartyRepublican                3.560e-04  2.948e-04   1.207
impeachment_1                  1.546e-03  2.342e-03   0.660
impeachment_2                  6.181e-03  2.556e-03   2.419
PartyRepublican:impeachment_1  2.194e-04  1.651e-03   0.133
PartyRepublican:impeachment_2 -3.549e-05  2.214e-03  -0.016

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.516                            
impechmnt_1 -0.085  0.029                     
impechmnt_2 -0.085  0.034  0.011              
PrtyRpbl:_1  0.043 -0.088 -0.301 -0.008       
PrtyRpbl:_2  0.041 -0.078 -0.006 -0.391  0.014
train.speaker.disgust.m1.pred <- dat_tidy.train.speaker.pred_grid %>%
  mutate(preds = predict(train.speaker.disgust.m1, newdata=dat_tidy.train.speaker.pred_grid, allow.new.levels=T))
  


train.speaker.disgust.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date')
Joining, by = c("date_int", "date", "date_int_scaled")

All sentiment




train.speaker.all.m1 <- dat_tidy.train.speaker %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1 | speaker) + (1 + Party | date_int) + (1 + impeachment_1  + impeachment_2 | sentiment), data=.)
Model failed to converge with max|grad| = 0.607011 (tol = 0.002, component 1)
summary(train.speaker.all.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int) + (1 + impeachment_1 +  
    impeachment_2 | sentiment)
   Data: .

REML criterion at convergence: -337962

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.1494 -0.5209 -0.1410  0.3412 12.8740 

Random effects:
 Groups    Name            Variance  Std.Dev. Corr      
 speaker   (Intercept)     2.339e-05 0.004837           
 date_int  (Intercept)     4.772e-06 0.002184           
           PartyRepublican 4.694e-06 0.002166 -0.29     
 sentiment (Intercept)     4.302e-04 0.020741           
           impeachment_1   3.693e-06 0.001922 0.65      
           impeachment_2   4.950e-05 0.007036 0.42  0.23
 Residual                  2.762e-04 0.016619           
Number of obs: 63417, groups:  speaker, 666; date_int, 145; sentiment, 10

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    0.0277547  0.0065691   4.225
PartyRepublican                0.0004757  0.0004813   0.989
impeachment_1                  0.0031754  0.0023994   1.323
impeachment_2                  0.0050971  0.0033087   1.540
PartyRepublican:impeachment_1 -0.0017793  0.0024291  -0.733
PartyRepublican:impeachment_2  0.0014619  0.0026687   0.548

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.034                            
impechmnt_1  0.162  0.017                     
impechmnt_2  0.281  0.015  0.047              
PrtyRpbl:_1  0.001 -0.047 -0.335 -0.004       
PrtyRpbl:_2  0.001 -0.046 -0.005 -0.290  0.010
convergence code: 0
Model failed to converge with max|grad| = 0.607011 (tol = 0.002, component 1)
dat_tidy.train.speaker.pred_grid.all_sent <- expand.grid(Party = unique(dat_tidy.train.speaker$Party), 
            speaker = 'new_speaker', 
            date_int = unique(dat_tidy.train.speaker$date_int),
            sentiment=unique(dat_tidy.train.speaker$sentiment))


dat_tidy.train.speaker.pred_grid.all_sent <- dat_tidy.train.speaker.pred_grid.all_sent %>%
  left_join(date_grid) %>%
  mutate(impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date_int"
train.speaker.all.m1.pred <- dat_tidy.train.speaker.pred_grid.all_sent %>%
  mutate(preds = predict(train.speaker.all.m1, 
                         newdata=dat_tidy.train.speaker.pred_grid.all_sent, 
                         allow.new.levels=T))
  
train.speaker.all.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date') + facet_wrap(sentiment~., ncol=2)
Joining, by = c("date_int", "date", "date_int_scaled")

train.speaker.all.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date') + facet_wrap(sentiment~., ncol=2, scales='free_y')
Joining, by = c("date_int", "date", "date_int_scaled")

sjPlot::plot_model(train.speaker.all.m1, type='re')[3]
[[1]]

Confirmation


dat_tidy.test.speaker <- dat_tidy.test %>%
  group_by(Party, speaker) %>%
  mutate(speaker_total_words = n()) %>%
  ungroup() %>%
  inner_join(nrc_sent) %>%
  count(Party, speaker, speaker_total_words, date, sentiment) %>%
  group_by(speaker, sentiment)
Joining, by = "word"
  

dat_tidy.test.speaker <- dat_tidy.test %>%
  filter(Party != 'Independent') %>%
  group_by(Party, speaker, date) %>%
  mutate(speaker_day_n = n()) %>%
  ungroup() %>%
  inner_join(nrc_sent) %>%
  count(Party, speaker, date, speaker_day_n, sentiment) %>%
  mutate(speaker_day_sent_prop = n/speaker_day_n)
Joining, by = "word"
# Create a date range from the min/max dates in our testing data
date_grid <- tibble(date = seq(min(dat_tidy.test.speaker$date), 
                               max(dat_tidy.test.speaker$date), by='days')) %>%
  mutate(date_int = row_number(), # Associate each date with an integer 
         date_int_scaled = date_int/100)

dat_tidy.test.speaker <- dat_tidy.test.speaker %>%
  left_join(date_grid) %>%
  mutate(date_int_scaled = date_int/100, 
         impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date"
  

Negative


test.speaker.negative.m1 <- dat_tidy.test.speaker %>%
  filter(sentiment=='negative') %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1  | speaker) + (1 + Party | date_int), data=.)
boundary (singular) fit: see ?isSingular
summary(test.speaker.negative.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int)
   Data: .

REML criterion at convergence: -34917.8

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.3963 -0.6268 -0.1663  0.4381 11.4546 

Random effects:
 Groups   Name            Variance  Std.Dev.  Corr 
 speaker  (Intercept)     2.261e-05 0.0047553      
 date_int (Intercept)     3.451e-05 0.0058743      
          PartyRepublican 6.270e-07 0.0007919 -1.00
 Residual                 3.021e-04 0.0173800      
Number of obs: 6743, groups:  speaker, 652; date_int, 143

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    0.0313529  0.0006947  45.134
PartyRepublican               -0.0002498  0.0006483  -0.385
impeachment_1                  0.0090151  0.0063993   1.409
impeachment_2                  0.0173639  0.0070045   2.479
PartyRepublican:impeachment_1 -0.0083012  0.0033936  -2.446
PartyRepublican:impeachment_2  0.0058701  0.0053485   1.098

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.546                            
impechmnt_1 -0.083  0.032                     
impechmnt_2 -0.078  0.031  0.009              
PrtyRpbl:_1  0.056 -0.090 -0.500 -0.008       
PrtyRpbl:_2  0.038 -0.062 -0.005 -0.506  0.014
convergence code: 0
boundary (singular) fit: see ?isSingular


dat_tidy.test.speaker.pred_grid <- expand.grid(Party = unique(dat_tidy.test.speaker$Party), 
            speaker = 'new_speaker', 
            date_int = unique(dat_tidy.test.speaker$date_int))


dat_tidy.test.speaker.pred_grid <- dat_tidy.test.speaker.pred_grid %>%
  left_join(date_grid) %>%
  mutate(impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date_int"
test.speaker.negative.m1.pred <- dat_tidy.test.speaker.pred_grid %>%
  mutate(preds = predict(test.speaker.negative.m1, newdata=dat_tidy.test.speaker.pred_grid, allow.new.levels=T))
  


test.speaker.negative.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of negative language for an average speaker' ) +
  ylab('Speaker proportion negative language') + 
  xlab('Date')
Joining, by = c("date_int", "date", "date_int_scaled")

NA

Disgust



test.speaker.disgust.m1 <- dat_tidy.test.speaker %>%
  filter(sentiment=='disgust') %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1  | speaker) + (1 + Party | date_int), data=.)
boundary (singular) fit: see ?isSingular
summary(test.speaker.disgust.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int)
   Data: .

REML criterion at convergence: -37469.3

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.7009 -0.5983 -0.2280  0.3244 11.5548 

Random effects:
 Groups   Name            Variance  Std.Dev. Corr 
 speaker  (Intercept)     5.295e-06 0.002301      
 date_int (Intercept)     4.316e-06 0.002078      
          PartyRepublican 1.797e-08 0.000134 -1.00
 Residual                 5.818e-05 0.007628      
Number of obs: 5497, groups:  speaker, 570; date_int, 143

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    0.0100217  0.0002945  34.027
PartyRepublican               -0.0002112  0.0003215  -0.657
impeachment_1                  0.0029204  0.0024163   1.209
impeachment_2                  0.0050296  0.0027192   1.850
PartyRepublican:impeachment_1 -0.0014567  0.0016049  -0.908
PartyRepublican:impeachment_2  0.0026842  0.0023911   1.123

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.566                            
impechmnt_1 -0.082  0.032                     
impechmnt_2 -0.074  0.030  0.010              
PrtyRpbl:_1  0.053 -0.089 -0.456 -0.008       
PrtyRpbl:_2  0.037 -0.065 -0.006 -0.511  0.014
convergence code: 0
boundary (singular) fit: see ?isSingular
test.speaker.disgust.m1.pred <- dat_tidy.test.speaker.pred_grid %>%
  mutate(preds = predict(test.speaker.disgust.m1, newdata=dat_tidy.test.speaker.pred_grid, allow.new.levels=T))
  


test.speaker.disgust.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date')
Joining, by = c("date_int", "date", "date_int_scaled")

All sentiment




test.speaker.all.m1 <- dat_tidy.test.speaker %>%
  lmer(speaker_day_sent_prop ~ 1 + Party*impeachment_1 + Party*impeachment_2  + (1 | speaker) + (1 + Party | date_int) + (1 + impeachment_1  + impeachment_2 | sentiment), data=.)
Model failed to converge with max|grad| = 0.0117501 (tol = 0.002, component 1)
summary(test.speaker.all.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: 
speaker_day_sent_prop ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int) + (1 + impeachment_1 +  
    impeachment_2 | sentiment)
   Data: .

REML criterion at convergence: -343711.1

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.3408 -0.5221 -0.1376  0.3464 13.1826 

Random effects:
 Groups    Name            Variance  Std.Dev. Corr      
 speaker   (Intercept)     3.767e-05 0.006137           
 date_int  (Intercept)     6.120e-06 0.002474           
           PartyRepublican 4.937e-06 0.002222 -0.60     
 sentiment (Intercept)     3.716e-04 0.019277           
           impeachment_1   1.127e-05 0.003357 0.84      
           impeachment_2   3.965e-05 0.006297 0.26  0.33
 Residual                  2.753e-04 0.016591           
Number of obs: 64495, groups:  speaker, 683; date_int, 144; sentiment, 10

Fixed effects:
                                Estimate Std. Error t value
(Intercept)                    0.0287953  0.0061115   4.712
PartyRepublican               -0.0002893  0.0005684  -0.509
impeachment_1                  0.0025763  0.0028124   0.916
impeachment_2                  0.0037042  0.0033973   1.090
PartyRepublican:impeachment_1 -0.0020211  0.0024625  -0.821
PartyRepublican:impeachment_2  0.0004820  0.0027752   0.174

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.049                            
impechmnt_1  0.313  0.022                     
impechmnt_2  0.147  0.020  0.080              
PrtyRpbl:_1  0.002 -0.041 -0.570 -0.006       
PrtyRpbl:_2  0.002 -0.040 -0.006 -0.503  0.010
convergence code: 0
Model failed to converge with max|grad| = 0.0117501 (tol = 0.002, component 1)
dat_tidy.test.speaker.pred_grid.all_sent <- expand.grid(Party = unique(dat_tidy.test.speaker$Party), 
            speaker = 'new_speaker', 
            date_int = unique(dat_tidy.test.speaker$date_int),
            sentiment=unique(dat_tidy.test.speaker$sentiment))


dat_tidy.test.speaker.pred_grid.all_sent <- dat_tidy.test.speaker.pred_grid.all_sent %>%
  left_join(date_grid) %>%
  mutate(impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "date_int"
test.speaker.all.m1.pred <- dat_tidy.test.speaker.pred_grid.all_sent %>%
  mutate(preds = predict(test.speaker.all.m1, 
                         newdata=dat_tidy.test.speaker.pred_grid.all_sent, 
                         allow.new.levels=T))
  
test.speaker.all.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date') + facet_wrap(sentiment~., ncol=2)
Joining, by = c("date_int", "date", "date_int_scaled")


test.speaker.all.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of disgust language for an average speaker' ) +
  ylab('Speaker proportion disgust language') + 
  xlab('Date') + facet_wrap(sentiment~., ncol=2, scales='free_y')
Joining, by = c("date_int", "date", "date_int_scaled")

sjPlot::plot_model(test.speaker.all.m1, type='re')[3]
[[1]]

Validation, validation, validation!

No matter what you’re trying to measure or what measurement methods you’re using, you should always closely examine what you are actually measuring.

Coincidentally, there’s a very relevant quote from Bill Clinton:

From the (Wikipedia entry)[https://en.wikipedia.org/wiki/Impeachment_of_Bill_Clinton] for Clinton’s impeachment

A much-quoted statement from Clinton’s grand jury testimony showed him questioning the precise use of the word “is”. Contending his statement that “there’s nothing going on between us” had been truthful because he had no ongoing relationship with Lewinsky at the time he was questioned, Clinton said: “It depends upon what the meaning of the word ‘is’ is…”

So what are we measuring when we count words? What are we measuring with NRC lexicon?

Let’s take a look!


text_dat <- readRDS('../data/tdta_clean_house_data.RDS')

top_docs <- dat_tidy.train %>%
  group_by(doc_num) %>%
  mutate(doc_total_words = n()) %>%
  ungroup() %>%
  inner_join(nrc_sent) %>%
  count(doc_num, doc_total_words, sentiment) %>%
  mutate(sent_prop = n/doc_total_words) %>%
  group_by(sentiment) %>%
  top_n(3, wt=n) %>%
  left_join(text_dat %>% select(doc_num, text))
Joining, by = "word"
Joining, by = "doc_num"
top_docs %>%
  filter(sentiment=='negative') %>%
  arrange(desc(sent_prop)) %>%
  mutate(text_seg = str_sub(text, 1,2000)) %>%
  select(-text) %>%
  View()
 

Because the documents are so long, it’s actually quite hard to evaluate the veracity of our measurement. Another option is to look at the most frequent sentiment words in our corpus.

One of the greatest strengths of dictionary-based text measurement methods is that they allow you to precisely define the construct you are interested in. This works extremely well when you are interested in specific words or types of words.

For example, if you are interested in function words, then it would never make sense to use anything other than a dictionary-based approach. Similarly, if you are truly interested in the usage of positive or negative words, then, again, it probably wouldn’t make sense to use anything other than a dictionary approach.

In these examples, there is a 1:1 relationship between the target construct and the operationalization. However, this 1:1 relationship is difficult to maintain for more abstract constructs, like “positive sentiment” or “negative sentiment”. In such cases, you (or someone else) has to decide which words evoke “positive sentiment” or “negative sentiment”.

Further, we are often interested in expressions of meaning that may operate above the word level. For instance, consider the following example:

`Let’s just say…I didn’t love it’

Most dictionary-based word count methods would estimate the sentiment expressed in this sentence as “positive” because of the token love. However, considering the entire context of this example, we can infer that the most likely sentiment is probably “negative”. Another issue related to context sensitivity is domain dependence: a word might have negative connotations in some discourse communities, but not in others.

In sum, dictionary-based word count approaches can be quite powerful; however, they have two notable shortcomings:

  1. Dependence on dictionary validity
  2. Cannot account for context

This does not mean that you shouldn’t use dictionary-based word count methods. However, it does mean that you should keep these short comings in mind. And, even better, you should try to account for them.

Doing better than simple word counts

In response to some of the issues raised above, people have started trying to improve on word count methods, for instance by accounting for negation or assigning weights to sentiment words. In R, you can use the sentimentr to do these things.

We’re not going to go into detail, but sentimentr operates on the sentence level, it provides the option of assigning continuous weights to words, and attempts to account for negation by looking for patterns in user-specified windows around sentiment words. It’s built lexicon is a combination of multiple lexicons (so it might have many of the same issues we observed in the NRC), but at least it tries to handle negation out of the box.

text_dat.sentr <- text_dat %>%
  filter(doc_num %!in% doc_ids_test) %>% # Select documents for training
  mutate(text = gsub('[Mm]r\\.|[Hh]\\.[Rr]\\.|[Nn][Uu][Mm]\\.', 'mr', text)) %>%
  mutate(sentences = get_sentences(text)) %$%
  sentiment_by(sentences, by= list(doc_num))

saveRDS(text_dat.sentr, file ='../data/text_dat_train_sentimentr_scores.RDS')
# For some reason looking at text_dat.sentr crashes my notebook, so
# let's look at it in the console

Let’s look at sentiment estimated with sentimentr at the day level.

dat_tidy.train %>%
  filter(Party != 'Independent') %>%
  distinct(doc_num, .keep_all = T) %>%
  left_join(text_dat.sentr) %>%
  group_by(Party, date) %>%
  summarize(mean_sent = mean(ave_sentiment)) %>%
  ggplot(aes(x = date, y = mean_sent, color=Party)) +
  geom_line() + 
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  geom_point() +
  theme_apa() +
  ggtitle('N of documents across time by party') +
  ylab('N') + 
  xlab('Date') +
  facet_wrap(Party~., ncol=1) +
  geom_hline(yintercept=0)
Joining, by = "doc_num"

Let’s run one of our models with this data and see what it tells us…

text_dat.sentr.train <- dat_tidy.train %>%
  filter(Party != 'Independent') %>%
  distinct(doc_num, .keep_all = T) %>%
  left_join(text_dat.sentr) %>%
  group_by(Party, speaker, date) %>%
  summarize(mean_sent = mean(ave_sentiment)) %>%
  left_join(date_grid) %>%
  mutate(date_int_scaled = date_int/100, 
         impeachment_1 = ifelse(date == as_datetime('1998-10-08'), 1, 0),
         impeachment_2 = ifelse(date == as_datetime('1998-12-19'), 1, 0))
Joining, by = "doc_num"
Joining, by = "date"
summary(text_dat.sentr.train.m1)
Linear mixed model fit by REML ['lmerMod']
Formula: mean_sent ~ 1 + Party * impeachment_1 + Party * impeachment_2 +  
    (1 | speaker) + (1 + Party | date_int)
   Data: text_dat.sentr.train

REML criterion at convergence: -7947.2

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.4500 -0.6075  0.0001  0.5915  6.2702 

Random effects:
 Groups   Name            Variance  Std.Dev. Corr 
 speaker  (Intercept)     1.627e-03 0.040334      
 date_int (Intercept)     1.416e-03 0.037629      
          PartyRepublican 8.741e-05 0.009349 -0.84
 Residual                 1.776e-02 0.133258      
Number of obs: 7171, groups:  speaker, 667; date_int, 145

Fixed effects:
                               Estimate Std. Error t value
(Intercept)                    0.135981   0.004920  27.636
PartyRepublican               -0.001476   0.005123  -0.288
impeachment_1                 -0.035166   0.041857  -0.840
impeachment_2                 -0.134203   0.046284  -2.900
PartyRepublican:impeachment_1  0.030942   0.027054   1.144
PartyRepublican:impeachment_2  0.008663   0.039290   0.220

Correlation of Fixed Effects:
            (Intr) PrtyRp impc_1 impc_2 PrR:_1
PartyRpblcn -0.604                            
impechmnt_1 -0.080  0.035                     
impechmnt_2 -0.077  0.036  0.010              
PrtyRpbl:_1  0.056 -0.078 -0.549 -0.009       
PrtyRpbl:_2  0.044 -0.061 -0.007 -0.557  0.013
text_dat.sentr.train.m1.pred %>%
  left_join(date_grid) %>%
  ggplot(aes(x = date, y = preds, color=Party)) + 
  geom_line() + 
  geom_point(aes(y = preds), alpha=.25) +
  theme_apa() + 
  scale_colour_manual(values = c("blue", "red")) + 
  theme_apa() +
  geom_vline(xintercept=as.numeric(as_datetime('1998-10-08')), linetype=2, alpha=.25) +
  geom_vline(xintercept=as.numeric(as_datetime('1998-12-19')), linetype=2, alpha=.25) +
  ggtitle('Daily expected proportion of negative language for an average speaker' ) +
  ylab('Speaker proportion negative language') + 
  xlab('Date')
Joining, by = c("date_int", "date", "date_int_scaled")

Loading LIWC dictionaries in R

It’s also possible to work with non-tidy dictionaries in R. For instance, we can use the package quanteda to load a LIWC format dictionary and get word counts. While there are a few ways to do this, we’ll load the dictionary into a quanteda object and then convert our training corpus into a quanteda corpus object, which is just a native quanteda format. We’ll then use quanteda to create a so-called document feature matrix or dfm, using our corpus and LIWC format dictionary.

  as.data.frame()
Error in as.data.frame() : argument "x" is missing, with no default
LS0tCnRpdGxlOiAiVGhlb3J5IERyaXZlbiBUZXh0IEFuYWx5c2lzIFdvcmtzaG9wIgpzdWJ0aXRsZTogIkRpY3Rpb25hcnkgTWV0aG9kcyAtLS0gV29yZCBjb3VudHMgXG5cblNQU1AgMjAyMCIKYXV0aG9yOiAKICBuYW1lOiAiSm9lIEhvb3ZlciAmIEJyZW5kYW4gS2VubmVkeSIKICBlbWFpbDogImpvc2VwaC5ob292ZXJAa2VsbG9nZy5ub3J0aHdlc3Rlcm4uZWR1XG5cbmJ0a2VubmVkQHVzYy5lZHUiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKLS0tCgoKIyBPdmVydmlldwoKSW4gdGhpcyBtb2R1bGUsIHdlIHdpbGwgZXhwbG9yZSBkaWN0aW9uYXJ5LWJhc2VkIHdvcmQgY291bnQgYXBwcm9hY2hlcyB0ZXh0IGFuYWx5c2lzIG1lYXN1cmVtZW50LiAKCldlIHdpbGwgYmUgd29ya2luZyB3aXRoIFUuUy4gSG91c2UgZmxvb3Igc3BlZWNoZXMgc3Bhbm5pbmcgSnVuZSAxOTk4IHRocm91Z2ggSnVseSAxOTk5LiBNb3JlIHNwZWNpZmljYWxseSwgd2Ugd2lsbDoKCigxKSBpbnZlc3RpZ2F0ZSB2YXJpYXRpb25zIGluIHNlbnRpbWVudCBhcyBhIGZ1bmN0aW9uIG9mIHBvbGl0aWNhbCBwYXJ0eQooMikgZXhwbG9yZSBzZW50aW1lbnQgZHluYW1pY3MgaW4gdGhlIGNvbnRleHQgb2YgUHJlc2lkZW50IEJpbGwgQ2xpbnRvbidzIGltcGVhY2htZW50LiAKCgojIEVudmlyb25tZW50IFByZXBhcmF0aW9uCmBgYHtyLCBlY2hvPUYsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQojIERlZmluZSBjaHVuayBvcHRpb25zCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvPVQsIG1lc3NhZ2U9Riwgd2FybmluZz1GKQpgYGAKCmBgYHtyLCBtZXNzYWdlPUYsICBlY2hvPVR9CiMgTG9hZCBwYWNrYWdlcyAKbGlicmFyeShwYWNtYW4pCnBfbG9hZChyZWFkciwgZHBseXIsIHRpZHlyLCBnZ3Bsb3QyLCBqdG9vbHMsIAogICAgICAga25pdHIsIHJlc2hhcGUyLCBqc29ubGl0ZSwgbHVicmlkYXRlLCBzZW50aW1lbnRyLCBtYWdyaXR0cikKCmBgYAoKIyBEYXRhIFByZXBhcmF0aW9uIAoKSW4gdGhlIHByZXByb2Nlc3NpbmcgbW9kdWxlIChgLi4vbm90ZWJvb2tzL3RkdGFfcHJlcHJvY2Vzc2luZy5SbWRgKSwgd2Ugc2F2ZWQgYSB0aWR5IGZvcm1hdCBkYXRhLmZyYW1lIGNvbnRhaW5pbmcgb3VyIGNvcnB1cy4gV2UnbGwgdXNlIHRoYXQgYXMgYSBzdGFydGluZyBwb2ludCBmb3Igb3VyIHdvcmsgaW4gdGhpcyBtb2R1bGUuCgpgYGB7cn0KCmRhdF90aWR5IDwtIHJlYWRSRFMoJy4uL2RhdGEvdGR0YV9jbGVhbl9ob3VzZV9kYXRhX3RpZHkuUkRTJykKCmRhdF90aWR5CgpgYGAKCgpJbiB0aGlzIGRhdGEuZnJhbWUsIHdvcmRzIGFyZSBzdG9yZWQgaW4gdGhlIGNlbGxzIG9mIHRoZSBjb2x1bW4gYHdvcmRgLiBBY2NvcmRpbmdseSwgYW4gZW50aXJlIGRvY3VtZW50LCBpZGVudGlmaWVkIGJ5IHRoZSB1bmlxdWUgZG9jdW1lbnQgaWRlbnRpZmllciBgZG9jX251bWAsIGlzIHN0b3JlZCBhY3Jvc3MgbXVsdGlwbGUgcm93cy4gVGhpcyBkYXRhLmZyYW1lIGFsc28gY29udGFpbnMgbWV0YWRhdGEgbGlrZSB0aGUgbmFtZSBvZiB0aGUgc3BlYWtlciBhc3NvY2lhdGVkIHdpdGggYSBnaXZlbiBkb2N1bWVudCwgYXMgd2VsbCBhcyB0aGUgdGhlIHNwZWFrZXIncyBwYXJ0eSwgZGlzdHJpY3QsIGFuZCBTdGF0ZS4KCgojIyBUcmFpbi9UZXN0IFNwbGl0IAoKSWYgeW91IGhhdmUgZW5vdWdoIGRhdGEsIHRoZXJlJ3MgcmVhbGx5IF9uZXZlcl8gYW55IHJlYXNvbiB0byBub3Qgc2VwYXJhdGUgeW91ciBkYXRhIGludG8gYSB0cmFpbmluZyBzZXQgYW5kIHRlc3Rpbmcgc2V0LiBPZiBjb3Vyc2UsIGRldGVybWluZyBob3cgbXVjaCBkYXRhIGlzICplbm91Z2gqIGNhbiBiZSB0cmlja3ksIGJlY2F1c2UgeW91IGRvbid0IHdhbnQgdG8gY3JlYXRlIGEgc2l0dWF0aW9uIHdoZXJlIHlvdSBhcmUgdW5kZXJwb3dlcmVkIG9yIHVuYWJsZSB0byBlc3RpbWF0ZSBhIHRhcmdldCBwYXJhbWV0ZXIgd2l0aCBzdWZmaWNpZW50IHByZWNpc2lvbi4gCgpIb3dldmVyLCB0cmVhdGluZyBkb2N1bWVudHMgYXMgb3VyIHVuaXRzIG9mIG9ic2VydmF0aW9uLCB3ZSBoYXZlIGFuICROID0gJCAzNSw5NTksIHdoaWNoIGlzIHByb2JhYmx5IGxhcmdlIGVub3VnaCBmb3Igc3BsaXR0aW5nLgoKYGBge3J9Cgpkb2NfaWRzIDwtIHVuaXF1ZShkYXRfdGlkeSRkb2NfbnVtKSAjIEdldCBkb2N1bWVudCBJRHMKCm5fZG9jcyA9IC41MCAqIGxlbmd0aChkb2NfaWRzKSAjIENhbGN1bGF0ZSBudW1iZXIgb2YgZG9jdW1lbnRzIHRvIHNhbXBsZQoKc2V0LnNlZWQoNTQzNSkgIyBzZXQgc2VlZCBmb3IgcmVwcm9kdWNpYmlsaXR5Cgpkb2NfaWRzX3Rlc3QgPSBzYW1wbGUoZG9jX2lkcywgbl9kb2NzKSAjIHNhbXBsZSBkb2N1bWVudCBJRHMgZm9yIHRlc3QgZGF0YQoKZGF0X3RpZHkudHJhaW4gPC0gZGF0X3RpZHkgJT4lCiAgZmlsdGVyKGRvY19udW0gJSFpbiUgZG9jX2lkc190ZXN0KSAjIFNlbGVjdCBkb2N1bWVudHMgZm9yIHRyYWluaW5nCgpkYXRfdGlkeS50ZXN0IDwtIGRhdF90aWR5ICU+JQogIGZpbHRlcihkb2NfbnVtICVpbiUgZG9jX2lkc190ZXN0KSAjIFNlbGVjdCBkb2N1bWVudHMgZm9yIHRlc3QKCgpgYGAKCgoKIyMgQ29uZHVjdGluZyBXb3JkIGNvdW50cwoKIyMjIEludHJvZHVjdGlvbiB0byBkaWN0aW9uYXJpZXMKClRvIGNvbmR1Y3Qgd29yZCBjb3VudCBhbmFseXNlcywgeW91IG5lZWQgYSAqZGljdGlvbmFyeSogb3IgKmxleGljb24qIHRoYXQgc3BlY2lmaWVzIHRoZSB3b3JkcyBhc3NvY2lhdGVkIHdpdGggeW91ciB0YXJnZXQgY29uc3RydWN0KHMpLiAKClRvIHN0YXJ0LCB3ZSdsbCB3b3JrIHdpdGggdGhlIE5SQyBzZW50aW1lbnQgZGljdGlvbmFyeSwgd2hpY2ggaXMgb25lIG9mIHRocmVlIHNlbnRpbWVudCBkaWN0aW9uYXJpZXMgcGFja2FnZWQgd2l0aCBgdGlkeXRleHRgOiAKCjEuIEFGSU5OCjIuIGJpbmcKMy4gbnJjCgpUbyBhY2Nlc3MgdGhlIE5SQyBkaWN0aW9uYXJ5LCB3ZSdsbCB1c2UgdGhlIGBnZXRfc2VudGltZW50c2AgZnVuY3Rpb24gdG8gc3RvcmUgdGhlIE5SQyBkaWN0aW9uYXJ5IGluIGFuIG9iamVjdCBjYWxsZWQgYG5yY19zZW50YC4KCmBgYHtyfQpucmNfc2VudCA8LSBnZXRfc2VudGltZW50cygnbnJjJykKbnJjX3NlbnQKYGBgCgpgbnJjX3NlbnRgIGNvbnRhaW5zIHR3byBjb2x1bW5zLCBgd29yZGAsIHdoaWNoIGNvbnRhaW5zIHRoZSB3b3JkcyBpbiB0aGUgZGljdGlvbmFyeSwgYW5kIGBzZW50aW1lbnRgLCB3aGljaCBzcGVjaWZpZXMgdGhlIHNlbnRpbWVudCBsYWJlbCBhc3NvY2lhdGVkIHdpdGggdGhlIHdvcmQuIAoKYGBge3J9Cm5yY19zZW50ICU+JQogIGNvdW50KHNlbnRpbWVudCkKYGBgCgpUaGUgTlJDIGRpY3Rpb25hcnkgY29udGFpbnMgMTAgc2VudGltZW50IGNhdGVnb3JpZXMgYW5kIGVhY2ggb2YgdGhlc2UgY2F0ZWdvcmllcyBoYXZlIHZhcnlpbmcgbnVtYmVycyBvZiB3b3JkcyBhc3NvY2lhdGVkIHdpdGggdGhlbS4gCgpMZXQncyB0YWtlIGEgZ2xhbmNlIGF0IGZpcnN0IHdvcmRzIGluIGVhY2ggY2F0ZWdvcnkgYnkgc3ByZWFkaW5nIG91ciB0aWR5IGRhdGEuZnJhbWU6CgpgYGB7cn0KbnJjX3NlbnQgJT4lCiAgZ3JvdXBfYnkoc2VudGltZW50KSAlPiUgCiAgbXV0YXRlKHRlbXBfaWQgPSByb3dfbnVtYmVyKCkpICU+JSAjIENyZWF0ZSBhIHRlbXBvcmFyeSBJRCB0byB3ZWlnaHQgdG9wX24gYnkKICB0b3BfbihuID0gLTUwLCB3dD10ZW1wX2lkKSAlPiUgICAgICMgR2V0IGZpcnN0IDUwIGl0ZW1zIGluIGVhY2ggZ3JvdXAgCiAgbXV0YXRlKHRlbXBfaWQgPSByb3dfbnVtYmVyKCkpICU+JSAjIENyZWF0ZSBhIHRlbXBvcmFyeSB1bmlxdWUgSUQgZm9yIGVhY2ggd29yZCBpbiBlYWNoIGdyb3VwCiAgdW5ncm91cCgpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSBzZW50aW1lbnQsIHZhbHVlc19mcm9tID0gd29yZCkgJT4lICMgU3ByZWFkIG91ciBkYXRhCiAgc2VsZWN0KC10ZW1wX2lkKQpgYGAKCgpHbGFuY2luZyBhdCB0aGVzZSB3b3JkcywgaXQncyBjbGVhciB0aGF0IHdvcmRzIGFyZSByZXBlYXRlZCBpbiBzb21lIGNhdGVnb3JpZXMuIAoKCjxicj4KCjxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MiIHJvbGU9ImFsZXJ0Ij4KICA8c3Ryb25nPlF1ZXN0aW9uOjwvc3Ryb25nPiBXaGF0IG90aGVyIGNoYXJhY3RlcmlzdGljcyBzdGFuZCBvdXQsIGlmIGFueT8KPC9kaXY+CgoKIyMjIGB0aWR5dGV4dGAgd29yZCBjb3VudCBzZW50aW1lbnQgYW5hbHlzaXMKCkluIHByaW5jaXBsZSwgYHRpZHl0ZXh0YCBtYWtlcyBzaW1wbGUgZGljdGlvbmFyeS1iYXNlZCB3b3JkIGNvdW50IHNlbnRpbWVudCBhbmFseXNpcyBxdWl0ZSBzaW1wbGUuIAoKVG8gY291bnQgdGhlIHdvcmRzIHdlIGNhbiBqdXN0OiAKCjEuIENvbmR1Y3QgYW4gYGlubmVyX2pvaW5gIGJldHdlZW4gb3VyIGRhdGEgYW5kIG91ciBzZW50aW1lbnQgZGljdGlvbmFyeQoyLiBDb3VudCB0aGUgbWF0Y2hlcwoKV2UnbGwgYWxzbyBkaXZpZGUgdGhlIG51bWJlciBvZiBtYXRjaGVzIGZvciBlYWNoIHNlbnRpbWVudCBkb21haW4gYnkgdGhlIHRvdGFsIG51bWJlciBvZiB3b3JkcyBpbiBvdXIgY29ycHVzLiBUaGlzIHdpbGwgdGVsbCB1cyB0aGUgcHJvcG9ydGlvbiBhc3NvY2lhdGVkIHdpdGggZWFjaCBzZW50aW1lbnQgZG9tYWluLgoKYGBge3J9Cgp0b3RhbF93b3JkcyA9IG5yb3coZGF0X3RpZHkudHJhaW4pCgpkYXRfdGlkeS50cmFpbiAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChzZW50aW1lbnQpICU+JQogIG11dGF0ZShwcm9wID0gbi90b3RhbF93b3JkcykgJT4lCiAgYXJyYW5nZShkZXNjKHByb3ApKQoKYGBgCgoKIyMjIEFmZmVjdGl2ZSBzZW50aW1lbnQgYnkgUG9saXRpY2FsIFBhcnR5CgpXZSBjYW4gYWxzbyBzdWJzZXQgb3VyIGRhdGEgaW4gb3JkZXIgdG8gYXNrIG1vcmUgc3BlY2lmaWMgcXVlc3Rpb25zLiBGb3IgaW5zdGFuY2UsIHdlIGNhbiBlYXNpbHkgZXN0aW1hdGUgc2VudGltZW50IHByb3BvcnRpb25zIGZvciBEZW1vY3JhdHMgYW5kIFJlcHVibGljYW5zLgoKYGBge3J9CgpkYXRfdGlkeS50cmFpbi5zZW50IDwtIGRhdF90aWR5LnRyYWluICU+JQogIGdyb3VwX2J5KFBhcnR5KSAlPiUgIyBHcm91cCBieSBQYXJ0eQogIG11dGF0ZSh0b3RhbF93b3JkcyA9IG4oKSkgJT4lICMgQ2FsY3VsYXRlIHRoZSB0b3RhbCB3b3JkcyBpbiBlYWNoIGdyb3VwCiAgdW5ncm91cCgpICU+JSAKICBpbm5lcl9qb2luKG5yY19zZW50KSAjIERyb3Agd29yZHMgdGhhdCBhcmVuJ3QgaW4gc2VudGltZW50IGRpY3Rpb25hcnkKICAKZGF0X3RpZHkudHJhaW4uc2VudCAlPiUgY291bnQodG90YWxfd29yZHMsIFBhcnR5LCBzZW50aW1lbnQpICU+JSAjIENvdW50IHRoZSBudW1iZXIgb2Ygcm93cyBpbiBlYWNoIFBhcnR5IGZvciBlYWNoIHNlbnRpbWVudAogIG11dGF0ZShwcm9wID0gbi90b3RhbF93b3JkcykgJT4lICMgQ2FsY3VsYXRlIHRoZSBwcm9wb3J0aW9uCiAgYXJyYW5nZShkZXNjKHByb3ApKSAlPiUgIyBBcnJhbmdlIGluIGRlc2NlbmRpbmcgb3JkZXIgYnkgcHJvcG9ydGlvbiBwb3NpdGl2ZQogIHNlbGVjdCgtbiwgLXRvdGFsX3dvcmRzKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tPSdQYXJ0eScsIHZhbHVlc19mcm9tID0gJ3Byb3AnKQoKCgpgYGAKCgpPdmVyYWxsLCBpdCBsb29rcyBsaWtlIHRoZXJlIGlzIHZlcnkgbGl0dGxlIG1lYW4gc2VudGltZW50IHZhcmlhdGlvbiBiZXR3ZWVuIFJlcHVibGljYW5zIGFuZCBEZW1vY3JhdHMuIEhvd2V2ZXIsIHdlJ3ZlIGNvbGxhcHNlZCBhY3Jvc3MgZG9jdW1lbnRzLiBUbyBnZXQgYSBiZXR0ZXIgaWRlYSBvZiBob3cgZXhwcmVzc2lvbnMgb2YgYWZmZWN0aXZlIHNlbnRpbWVudCB2YXJ5IGFjcm9zcyBQYXJ0aWVzLCBsZXQncyB2aXN1YWxpemUgdGhlIGRpc3RyaWJ1dGlvbiBvZiBzZW50aW1lbnQgaW4gZG9jdW1lbnRzIAoKCgpgYGB7cn0KIApkYXRfdGlkeS50cmFpbi5zZW50ICU+JQogIGZpbHRlcihQYXJ0eSAhPSdJbmRlcGVuZGVudCcpICU+JQogIGNvdW50KHRvdGFsX3dvcmRzLCBQYXJ0eSwgZG9jX251bSwgc2VudGltZW50KSAlPiUKICBtdXRhdGUocHJvcCA9IG4vdG90YWxfd29yZHMpICU+JQogIGdncGxvdChhZXMoeSA9IHByb3AsIHggPSBQYXJ0eSwgY29sb3I9UGFydHkpKSArIAogIGdlb21faml0dGVyKGFscGhhPS4yNSkgKyAKICBmYWNldF93cmFwKC5+c2VudGltZW50LCBuY29sPTUpICsgCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKCJibHVlIiwgInJlZCIpKSArCiAgdGhlbWVfYXBhKCkgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCBoanVzdCA9IDEpKSArIAogIGdndGl0bGUoJ0RvY3VtZW50LWxldmVsIHByb3BvcnRpb25zIG9mIHNlbnRpbWVudCB3b3JkcyBieSBwYXJ0eScpICsgCiAgeGxhYignUGFydHknKSArIAogIHlsYWIoJ1Byb3BvcnRpb24nKQoKYGBgCgoKV2hhdCBpZiB3ZSBsb29rIGF0IHRoZSBkb2N1bWVudCAqTipzIG9mIHNlbnRpbWVudCB3b3JkcyBpbnN0ZWFkIG9mIHByb3BvcnRpb25zPwoKYGBge3J9CiAKZGF0X3RpZHkudHJhaW4uc2VudCAlPiUKICBmaWx0ZXIoUGFydHkgIT0nSW5kZXBlbmRlbnQnKSAlPiUKICBjb3VudCh0b3RhbF93b3JkcywgUGFydHksIGRvY19udW0sIHNlbnRpbWVudCkgJT4lCiAgbXV0YXRlKHByb3AgPSBuL3RvdGFsX3dvcmRzKSAlPiUKICBnZ3Bsb3QoYWVzKHkgPSBuLCB4ID0gUGFydHksIGNvbG9yPVBhcnR5KSkgKyAKICBnZW9tX2ppdHRlcihhbHBoYT0uMjUpICsgCiAgZmFjZXRfd3JhcCgufnNlbnRpbWVudCwgbmNvbD01KSArIAogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gYygiYmx1ZSIsICJyZWQiKSkgKwogIHRoZW1lKCkgKyAKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCBoanVzdCA9IDEpKSArIAogIGdndGl0bGUoJ0RvY3VtZW50LWxldmVsIE5zIG9mIHNlbnRpbWVudCB3b3JkcyBieSBwYXJ0eScpICsgCiAgeGxhYignUGFydHknKSArIAogIHlsYWIoJ04nKQoKYGBgCgoKCjxicj4KCjxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MiIHJvbGU9ImFsZXJ0Ij4KICA8c3Ryb25nPlF1ZXN0aW9uOjwvc3Ryb25nPiBXaHkgbWlnaHQgd2Ugd2FudCB0byBsb29rIGF0IHByb3BvcnRpb24gdnMuIE4gKG9yIHZpY2UgdmVyc2EpPwo8L2Rpdj4KCgojIyMgRHluYW1pY3MgaW4gYWZmZWN0aXZlIHNlbnRpbWVudCBieSBwb2xpdGljYWwgcGFydHkKCkNsZWFybHksIHRoZXJlIGlzbid0IG11Y2ggbWFyZ2luYWwgYmV0d2VlbiBncm91cCB2YXJpYXRpb24gaW4gYWZmZWN0aXZlIHNlbnRpbWVudCBpbiB0aGlzIGRhdGFzZXQuIEhvd2V2ZXIsIG1heWJlIHRoZXJlIGFyZSBpbnRlcmVzdGluZyBlZmZlY3RzIG9wZXJhdGluZyBhdCBvdGhlciBsZXZlbHMhCgpMZXQncyBleGFtaW5lIHRlbXBvcmFsIHZhcmlhdGlvbiBpbiBhZmZlY3RpdmUgc2VudGltZW50IGJldHdlZW4gcGFydGllcy4gVG8gZG8gdGhpcywgd2Ugd2lsbCB0YWtlIGEgc2ltaWxhciBhcHByb2FjaCwgYnV0IGluc3RlYWQgb2YgY291bnRpbmcgdGhlIHNlbnRpbWVudCBpbiBlYWNoIGRvY3VtZW50LCB3ZSdsbCBjb3VudCB0aGUgc2VudGltZW50IG9uIGVhY2ggb2JzZXJ2ZWQgZGF5LiAKCkZpcnN0LCBob3dldmVyLCBsZXQncyBnbGFuY2UgYXQgdGhlIGRpc3RyaWJ1dGlvbiBvZiBkb2N1bWVudHMgYWNyb3NzIHRpbWUuIEZvciByZWZlcmVuY2UsIGxldCdzIGFsc28gYWRkIHZlcnRpY2FsIGxpbmVzIHRvIGluZGljYXRlIHRoZSBkYXRlcyBvbiB3aGljaCBDbGludG9uJ3MgaW1wZWFjaG1lbnQgd2FzIGluaWF0ZWQgYW5kIHZvdGVkIG9uLgoKCgpgYGB7ciwgZmlnLndpZHRoPTEwfQoKc2VudF90aW1lIDwtIGRhdF90aWR5LnRyYWluLnNlbnQgJT4lCiAgZmlsdGVyKFBhcnR5ICE9J0luZGVwZW5kZW50JykgJT4lCiAgZGlzdGluY3QoZG9jX251bSwgLmtlZXBfYWxsID0gVCkgJT4lCiAgY291bnQoZGF0ZSwgUGFydHkpICU+JQogIGdncGxvdChhZXMoeSA9IG4sIHggPSBkYXRlLCBjb2xvcj1QYXJ0eSkpICsgCiAgZ2VvbV9saW5lKGFscGhhPS41KSArIAogIGZhY2V0X3dyYXAoUGFydHkgfi4gLCBuY29sPTEpICsKICB0aGVtZV9hcGEoKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZW9tX3BvaW50KCkgKwogIHRoZW1lX2FwYSgpICsKICBnZ3RpdGxlKCdOIG9mIGRvY3VtZW50cyBhY3Jvc3MgdGltZSBieSBwYXJ0eScpICsKICB5bGFiKCdOJykgKyAKICB4bGFiKCdEYXRlJykKCmdncGxvdGx5KHNlbnRfdGltZSkKCgpgYGAKCkNsZWFybHksIHRoZXJlIGlzIHN1YnN0YW50aWFsIHZhcmlhdGlvbiBpbiB0aGUgbnVtYmVyIG9mIGRvY3VtZW50cyAoaS5lLiBzcGVlY2hlcyBnaXZlbiBieSBpbmRpdmlkdWFsIHNwZWFrZXJzKSBhY3Jvc3MgdGltZS4gCgoKPGJyPgoKPGRpdiBjbGFzcz0iYWxlcnQgYWxlcnQtc3VjY2VzcyIgcm9sZT0iYWxlcnQiPgogIDxzdHJvbmc+UXVlc3Rpb246PC9zdHJvbmc+IFdoYXQgYXJlIG91ciBzYW1wbGUgc2l6ZXMgb24gdGhlIGltcGVhY2htZW50LXJlbGV2YW50IGRheXM/CjwvZGl2PgoKCgpOb3csIGxldCdzIHBsb3Qgc2VudGltZW50IGFjcm9zcyB0aW1lIGJ5IHBhcnR5LiAKCmBgYHtyLCBmaWcuaGVpZ2h0PTEwLCBmaWcud2lkdGg9MTB9CgpkYXRfdGlkeS50cmFpbi5zZW50ICU+JQogIGZpbHRlcihQYXJ0eSAhPSdJbmRlcGVuZGVudCcpICU+JQogIGNvdW50KHRvdGFsX3dvcmRzLCBQYXJ0eSwgZGF0ZSwgc2VudGltZW50KSAlPiUKICBtdXRhdGUocHJvcCA9IG4vdG90YWxfd29yZHMpICU+JQogIGdncGxvdChhZXMoeSA9IG4sIHggPSBkYXRlLCBjb2xvcj1QYXJ0eSkpICsgCiAgZmFjZXRfd3JhcChzZW50aW1lbnR+UGFydHksIG5jb2w9NCkgKyAKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoImJsdWUiLCAicmVkIikpICsKICB0aGVtZSgpICsgCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA0NSwgaGp1c3QgPSAxKSkgKyAKICBnZ3RpdGxlKCdEb2N1bWVudC1sZXZlbCBOcyBvZiBzZW50aW1lbnQgd29yZHMgYnkgcGFydHknKSArIAogIHhsYWIoJ1BhcnR5JykgKyAKICB5bGFiKCdOJykgKyAKICBnZW9tX3Ntb290aChjb2xvcj0nYmxhY2snKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSksIGxpbmV0eXBlPTIpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMi0xOScpKSwgbGluZXR5cGU9MikgKwogIGdlb21fcG9pbnQoYWxwaGE9LjI1KSAKCmBgYAoKCgpgYGB7ciwgZmlnLmhlaWdodD0xMCwgZmlnLndpZHRoPTEwfQoKZGF0X3RpZHkudHJhaW4uc2VudCAlPiUKICBmaWx0ZXIoUGFydHkgIT0nSW5kZXBlbmRlbnQnKSAlPiUKICBjb3VudCh0b3RhbF93b3JkcywgUGFydHksIGRhdGUsIHNlbnRpbWVudCkgJT4lCiAgbXV0YXRlKHByb3AgPSBuL3RvdGFsX3dvcmRzKSAlPiUKICBnZ3Bsb3QoYWVzKHkgPSBwcm9wLCB4ID0gZGF0ZSwgY29sb3I9UGFydHkpKSArIAogIGZhY2V0X3dyYXAoc2VudGltZW50flBhcnR5LCBuY29sPTQpICsgCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKCJibHVlIiwgInJlZCIpKSArCiAgdGhlbWUoKSArIAogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNDUsIGhqdXN0ID0gMSkpICsgCiAgZ2d0aXRsZSgnRG9jdW1lbnQtbGV2ZWwgcHJvcG9ydGlvbnMgb2Ygc2VudGltZW50IHdvcmRzIGJ5IHBhcnR5JykgKyAKICB4bGFiKCdQYXJ0eScpICsgCiAgeWxhYignTicpICsgCiAgZ2VvbV9zbW9vdGgoY29sb3I9J2JsYWNrJykgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JykpLCBsaW5ldHlwZT0yKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSksIGxpbmV0eXBlPTIpICsKICBnZW9tX3BvaW50KGFscGhhPS4yNSkgCgpgYGAKCgo8YnI+Cgo8ZGl2IGNsYXNzPSJhbGVydCBhbGVydC1zdWNjZXNzIiByb2xlPSJhbGVydCI+CiAgPHN0cm9uZz5RdWVzdGlvbjo8L3N0cm9uZz4gV2hhdCBjYW4gd2UgbGVhcm4gZnJvbSB0aGlzIGZpZ3VyZT8KPC9kaXY+CgoKIyMgSHlwb3RoZXNpcyB0ZXN0aW5nIHdpdGggd29yZCBjb3VudHMKCmBgYHtyfQoKZGF0X3RpZHkudHJhaW4uc3BlYWtlciA8LSBkYXRfdGlkeS50cmFpbiAlPiUKICBncm91cF9ieShQYXJ0eSwgc3BlYWtlcikgJT4lCiAgbXV0YXRlKHNwZWFrZXJfdG90YWxfd29yZHMgPSBuKCkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChQYXJ0eSwgc3BlYWtlciwgc3BlYWtlcl90b3RhbF93b3JkcywgZGF0ZSwgc2VudGltZW50KSAlPiUKICBncm91cF9ieShzcGVha2VyLCBzZW50aW1lbnQpICU+JQogIG11dGF0ZShzcGVha2VyX3NlbnRfbWVhbnMgPSBtZWFuKG4pLCAKICAgICAgICAgc3BlYWtlcl9zZW50X2NudHIgPSBuIC0gc3BlYWtlcl9zZW50X21lYW5zKQogIApgYGAKCgoKYGBge3J9CgpkYXRfdGlkeS50cmFpbi5zcGVha2VyIDwtIGRhdF90aWR5LnRyYWluICU+JQogIGZpbHRlcihQYXJ0eSAhPSAnSW5kZXBlbmRlbnQnKSAlPiUKICBncm91cF9ieShQYXJ0eSwgc3BlYWtlciwgZGF0ZSkgJT4lCiAgbXV0YXRlKHNwZWFrZXJfZGF5X24gPSBuKCkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChQYXJ0eSwgc3BlYWtlciwgZGF0ZSwgc3BlYWtlcl9kYXlfbiwgc2VudGltZW50KSAlPiUKICBtdXRhdGUoc3BlYWtlcl9kYXlfc2VudF9wcm9wID0gbi9zcGVha2VyX2RheV9uKQoKCgoKIyBDcmVhdGUgYSBkYXRlIHJhbmdlIGZyb20gdGhlIG1pbi9tYXggZGF0ZXMgaW4gb3VyIHRyYWluaW5nIGRhdGEKZGF0ZV9ncmlkIDwtIHRpYmJsZShkYXRlID0gc2VxKG1pbihkYXRfdGlkeS50cmFpbi5zcGVha2VyJGRhdGUpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1heChkYXRfdGlkeS50cmFpbi5zcGVha2VyJGRhdGUpLCBieT0nZGF5cycpKSAlPiUKICBtdXRhdGUoZGF0ZV9pbnQgPSByb3dfbnVtYmVyKCksICMgQXNzb2NpYXRlIGVhY2ggZGF0ZSB3aXRoIGFuIGludGVnZXIgCiAgICAgICAgIGRhdGVfaW50X3NjYWxlZCA9IGRhdGVfaW50LzEwMCkKCmRhdF90aWR5LnRyYWluLnNwZWFrZXIgPC0gZGF0X3RpZHkudHJhaW4uc3BlYWtlciAlPiUKICBsZWZ0X2pvaW4oZGF0ZV9ncmlkKSAlPiUKICBtdXRhdGUoZGF0ZV9pbnRfc2NhbGVkID0gZGF0ZV9pbnQvMTAwLCAKICAgICAgICAgaW1wZWFjaG1lbnRfMSA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JyksIDEsIDApLAogICAgICAgICBpbXBlYWNobWVudF8yID0gaWZlbHNlKGRhdGUgPT0gYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSwgMSwgMCkpCiAgCmBgYAoKCiMjIyBOZWdhdGl2ZSBzZW50aW1lbnQKCmBgYHtyfQoKdHJhaW4uc3BlYWtlci5uZWdhdGl2ZS5tMSA8LSBkYXRfdGlkeS50cmFpbi5zcGVha2VyICU+JQogIGZpbHRlcihzZW50aW1lbnQ9PSduZWdhdGl2ZScpICU+JQogIGxtZXIoc3BlYWtlcl9kYXlfc2VudF9wcm9wIH4gMSArIFBhcnR5KmltcGVhY2htZW50XzEgKyBQYXJ0eSppbXBlYWNobWVudF8yICArICgxICB8IHNwZWFrZXIpICsgKDEgKyBQYXJ0eSB8IGRhdGVfaW50KSwgZGF0YT0uKQoKc3VtbWFyeSh0cmFpbi5zcGVha2VyLm5lZ2F0aXZlLm0xKQoKYGBgCgoKCmBgYHtyfQoKCmRhdF90aWR5LnRyYWluLnNwZWFrZXIucHJlZF9ncmlkIDwtIGV4cGFuZC5ncmlkKFBhcnR5ID0gdW5pcXVlKGRhdF90aWR5LnRyYWluLnNwZWFrZXIkUGFydHkpLCAKICAgICAgICAgICAgc3BlYWtlciA9ICduZXdfc3BlYWtlcicsIAogICAgICAgICAgICBkYXRlX2ludCA9IHVuaXF1ZShkYXRfdGlkeS50cmFpbi5zcGVha2VyJGRhdGVfaW50KSkKCgpkYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCA8LSBkYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCAlPiUKICBsZWZ0X2pvaW4oZGF0ZV9ncmlkKSAlPiUKICBtdXRhdGUoaW1wZWFjaG1lbnRfMSA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JyksIDEsIDApLAogICAgICAgICBpbXBlYWNobWVudF8yID0gaWZlbHNlKGRhdGUgPT0gYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSwgMSwgMCkpCgoKCnRyYWluLnNwZWFrZXIubmVnYXRpdmUubTEucHJlZCA8LSBkYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCAlPiUKICBtdXRhdGUocHJlZHMgPSBwcmVkaWN0KHRyYWluLnNwZWFrZXIubmVnYXRpdmUubTEsIG5ld2RhdGE9ZGF0X3RpZHkudHJhaW4uc3BlYWtlci5wcmVkX2dyaWQsIGFsbG93Lm5ldy5sZXZlbHM9VCkpCiAgCgoKdHJhaW4uc3BlYWtlci5uZWdhdGl2ZS5tMS5wcmVkICU+JQogIGxlZnRfam9pbihkYXRlX2dyaWQpICU+JQogIGdncGxvdChhZXMoeCA9IGRhdGUsIHkgPSBwcmVkcywgY29sb3I9UGFydHkpKSArIAogIGdlb21fbGluZSgpICsgCiAgZ2VvbV9wb2ludChhZXMoeSA9IHByZWRzKSwgYWxwaGE9LjI1KSArCiAgdGhlbWVfYXBhKCkgKyAKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoImJsdWUiLCAicmVkIikpICsgCiAgdGhlbWVfYXBhKCkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMi0xOScpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2d0aXRsZSgnRGFpbHkgZXhwZWN0ZWQgcHJvcG9ydGlvbiBvZiBuZWdhdGl2ZSBsYW5ndWFnZSBmb3IgYW4gYXZlcmFnZSBzcGVha2VyJyApICsKICB5bGFiKCdTcGVha2VyIHByb3BvcnRpb24gbmVnYXRpdmUgbGFuZ3VhZ2UnKSArIAogIHhsYWIoJ0RhdGUnKQoKICAKYGBgCgoKCiMjIyBEaXNndXN0IAoKYGBge3J9CgoKdHJhaW4uc3BlYWtlci5kaXNndXN0Lm0xIDwtIGRhdF90aWR5LnRyYWluLnNwZWFrZXIgJT4lCiAgZmlsdGVyKHNlbnRpbWVudD09J2Rpc2d1c3QnKSAlPiUKICBsbWVyKHNwZWFrZXJfZGF5X3NlbnRfcHJvcCB+IDEgKyBQYXJ0eSppbXBlYWNobWVudF8xICsgUGFydHkqaW1wZWFjaG1lbnRfMiAgKyAoMSAgfCBzcGVha2VyKSArICgxICsgUGFydHkgfCBkYXRlX2ludCksIGRhdGE9LikKCnN1bW1hcnkodHJhaW4uc3BlYWtlci5kaXNndXN0Lm0xKQoKCgp0cmFpbi5zcGVha2VyLmRpc2d1c3QubTEucHJlZCA8LSBkYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCAlPiUKICBtdXRhdGUocHJlZHMgPSBwcmVkaWN0KHRyYWluLnNwZWFrZXIuZGlzZ3VzdC5tMSwgbmV3ZGF0YT1kYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCwgYWxsb3cubmV3LmxldmVscz1UKSkKICAKCgp0cmFpbi5zcGVha2VyLmRpc2d1c3QubTEucHJlZCAlPiUKICBsZWZ0X2pvaW4oZGF0ZV9ncmlkKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBkYXRlLCB5ID0gcHJlZHMsIGNvbG9yPVBhcnR5KSkgKyAKICBnZW9tX2xpbmUoKSArIAogIGdlb21fcG9pbnQoYWVzKHkgPSBwcmVkcyksIGFscGhhPS4yNSkgKwogIHRoZW1lX2FwYSgpICsgCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKCJibHVlIiwgInJlZCIpKSArIAogIHRoZW1lX2FwYSgpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdndGl0bGUoJ0RhaWx5IGV4cGVjdGVkIHByb3BvcnRpb24gb2YgZGlzZ3VzdCBsYW5ndWFnZSBmb3IgYW4gYXZlcmFnZSBzcGVha2VyJyApICsKICB5bGFiKCdTcGVha2VyIHByb3BvcnRpb24gZGlzZ3VzdCBsYW5ndWFnZScpICsgCiAgeGxhYignRGF0ZScpCgoKYGBgCgoKIyMjIEFsbCBzZW50aW1lbnQKCmBgYHtyfQoKCgp0cmFpbi5zcGVha2VyLmFsbC5tMSA8LSBkYXRfdGlkeS50cmFpbi5zcGVha2VyICU+JQogIGxtZXIoc3BlYWtlcl9kYXlfc2VudF9wcm9wIH4gMSArIFBhcnR5KmltcGVhY2htZW50XzEgKyBQYXJ0eSppbXBlYWNobWVudF8yICArICgxIHwgc3BlYWtlcikgKyAoMSArIFBhcnR5IHwgZGF0ZV9pbnQpICsgKDEgKyBpbXBlYWNobWVudF8xICArIGltcGVhY2htZW50XzIgfCBzZW50aW1lbnQpLCBkYXRhPS4pCgpzdW1tYXJ5KHRyYWluLnNwZWFrZXIuYWxsLm0xKQoKCmRhdF90aWR5LnRyYWluLnNwZWFrZXIucHJlZF9ncmlkLmFsbF9zZW50IDwtIGV4cGFuZC5ncmlkKFBhcnR5ID0gdW5pcXVlKGRhdF90aWR5LnRyYWluLnNwZWFrZXIkUGFydHkpLCAKICAgICAgICAgICAgc3BlYWtlciA9ICduZXdfc3BlYWtlcicsIAogICAgICAgICAgICBkYXRlX2ludCA9IHVuaXF1ZShkYXRfdGlkeS50cmFpbi5zcGVha2VyJGRhdGVfaW50KSwKICAgICAgICAgICAgc2VudGltZW50PXVuaXF1ZShkYXRfdGlkeS50cmFpbi5zcGVha2VyJHNlbnRpbWVudCkpCgoKZGF0X3RpZHkudHJhaW4uc3BlYWtlci5wcmVkX2dyaWQuYWxsX3NlbnQgPC0gZGF0X3RpZHkudHJhaW4uc3BlYWtlci5wcmVkX2dyaWQuYWxsX3NlbnQgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgbXV0YXRlKGltcGVhY2htZW50XzEgPSBpZmVsc2UoZGF0ZSA9PSBhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpLCAxLCAwKSwKICAgICAgICAgaW1wZWFjaG1lbnRfMiA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JyksIDEsIDApKQoKCnRyYWluLnNwZWFrZXIuYWxsLm0xLnByZWQgPC0gZGF0X3RpZHkudHJhaW4uc3BlYWtlci5wcmVkX2dyaWQuYWxsX3NlbnQgJT4lCiAgbXV0YXRlKHByZWRzID0gcHJlZGljdCh0cmFpbi5zcGVha2VyLmFsbC5tMSwgCiAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhPWRhdF90aWR5LnRyYWluLnNwZWFrZXIucHJlZF9ncmlkLmFsbF9zZW50LCAKICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93Lm5ldy5sZXZlbHM9VCkpCiAgCgpgYGAKCgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD02fQp0cmFpbi5zcGVha2VyLmFsbC5tMS5wcmVkICU+JQogIGxlZnRfam9pbihkYXRlX2dyaWQpICU+JQogIGdncGxvdChhZXMoeCA9IGRhdGUsIHkgPSBwcmVkcywgY29sb3I9UGFydHkpKSArIAogIGdlb21fbGluZSgpICsgCiAgZ2VvbV9wb2ludChhZXMoeSA9IHByZWRzKSwgYWxwaGE9LjI1KSArCiAgdGhlbWVfYXBhKCkgKyAKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoImJsdWUiLCAicmVkIikpICsgCiAgdGhlbWVfYXBhKCkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMi0xOScpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2d0aXRsZSgnRGFpbHkgZXhwZWN0ZWQgcHJvcG9ydGlvbiBvZiBkaXNndXN0IGxhbmd1YWdlIGZvciBhbiBhdmVyYWdlIHNwZWFrZXInICkgKwogIHlsYWIoJ1NwZWFrZXIgcHJvcG9ydGlvbiBkaXNndXN0IGxhbmd1YWdlJykgKyAKICB4bGFiKCdEYXRlJykgKyBmYWNldF93cmFwKHNlbnRpbWVudH4uLCBuY29sPTIpCgpgYGAKCgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD02fQp0cmFpbi5zcGVha2VyLmFsbC5tMS5wcmVkICU+JQogIGxlZnRfam9pbihkYXRlX2dyaWQpICU+JQogIGdncGxvdChhZXMoeCA9IGRhdGUsIHkgPSBwcmVkcywgY29sb3I9UGFydHkpKSArIAogIGdlb21fbGluZSgpICsgCiAgZ2VvbV9wb2ludChhZXMoeSA9IHByZWRzKSwgYWxwaGE9LjI1KSArCiAgdGhlbWVfYXBhKCkgKyAKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoImJsdWUiLCAicmVkIikpICsgCiAgdGhlbWVfYXBhKCkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMi0xOScpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2d0aXRsZSgnRGFpbHkgZXhwZWN0ZWQgcHJvcG9ydGlvbiBvZiBkaXNndXN0IGxhbmd1YWdlIGZvciBhbiBhdmVyYWdlIHNwZWFrZXInICkgKwogIHlsYWIoJ1NwZWFrZXIgcHJvcG9ydGlvbiBkaXNndXN0IGxhbmd1YWdlJykgKyAKICB4bGFiKCdEYXRlJykgKyBmYWNldF93cmFwKHNlbnRpbWVudH4uLCBuY29sPTIsIHNjYWxlcz0nZnJlZV95JykKCgpgYGAKCgoKYGBge3J9CnNqUGxvdDo6cGxvdF9tb2RlbCh0cmFpbi5zcGVha2VyLmFsbC5tMSwgdHlwZT0ncmUnKVszXQpgYGAKCgojIyBDb25maXJtYXRpb24gCgoKYGBge3J9CgpkYXRfdGlkeS50ZXN0LnNwZWFrZXIgPC0gZGF0X3RpZHkudGVzdCAlPiUKICBncm91cF9ieShQYXJ0eSwgc3BlYWtlcikgJT4lCiAgbXV0YXRlKHNwZWFrZXJfdG90YWxfd29yZHMgPSBuKCkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChQYXJ0eSwgc3BlYWtlciwgc3BlYWtlcl90b3RhbF93b3JkcywgZGF0ZSwgc2VudGltZW50KSAlPiUKICBncm91cF9ieShzcGVha2VyLCBzZW50aW1lbnQpCiAgCmBgYAoKCgpgYGB7cn0KCmRhdF90aWR5LnRlc3Quc3BlYWtlciA8LSBkYXRfdGlkeS50ZXN0ICU+JQogIGZpbHRlcihQYXJ0eSAhPSAnSW5kZXBlbmRlbnQnKSAlPiUKICBncm91cF9ieShQYXJ0eSwgc3BlYWtlciwgZGF0ZSkgJT4lCiAgbXV0YXRlKHNwZWFrZXJfZGF5X24gPSBuKCkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChQYXJ0eSwgc3BlYWtlciwgZGF0ZSwgc3BlYWtlcl9kYXlfbiwgc2VudGltZW50KSAlPiUKICBtdXRhdGUoc3BlYWtlcl9kYXlfc2VudF9wcm9wID0gbi9zcGVha2VyX2RheV9uKQoKCgoKIyBDcmVhdGUgYSBkYXRlIHJhbmdlIGZyb20gdGhlIG1pbi9tYXggZGF0ZXMgaW4gb3VyIHRlc3RpbmcgZGF0YQpkYXRlX2dyaWQgPC0gdGliYmxlKGRhdGUgPSBzZXEobWluKGRhdF90aWR5LnRlc3Quc3BlYWtlciRkYXRlKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXgoZGF0X3RpZHkudGVzdC5zcGVha2VyJGRhdGUpLCBieT0nZGF5cycpKSAlPiUKICBtdXRhdGUoZGF0ZV9pbnQgPSByb3dfbnVtYmVyKCksICMgQXNzb2NpYXRlIGVhY2ggZGF0ZSB3aXRoIGFuIGludGVnZXIgCiAgICAgICAgIGRhdGVfaW50X3NjYWxlZCA9IGRhdGVfaW50LzEwMCkKCmRhdF90aWR5LnRlc3Quc3BlYWtlciA8LSBkYXRfdGlkeS50ZXN0LnNwZWFrZXIgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgbXV0YXRlKGRhdGVfaW50X3NjYWxlZCA9IGRhdGVfaW50LzEwMCwgCiAgICAgICAgIGltcGVhY2htZW50XzEgPSBpZmVsc2UoZGF0ZSA9PSBhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpLCAxLCAwKSwKICAgICAgICAgaW1wZWFjaG1lbnRfMiA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JyksIDEsIDApKQogIApgYGAKCgojIyMgTmVnYXRpdmUKCmBgYHtyfQoKdGVzdC5zcGVha2VyLm5lZ2F0aXZlLm0xIDwtIGRhdF90aWR5LnRlc3Quc3BlYWtlciAlPiUKICBmaWx0ZXIoc2VudGltZW50PT0nbmVnYXRpdmUnKSAlPiUKICBsbWVyKHNwZWFrZXJfZGF5X3NlbnRfcHJvcCB+IDEgKyBQYXJ0eSppbXBlYWNobWVudF8xICsgUGFydHkqaW1wZWFjaG1lbnRfMiAgKyAoMSAgfCBzcGVha2VyKSArICgxICsgUGFydHkgfCBkYXRlX2ludCksIGRhdGE9LikKCnN1bW1hcnkodGVzdC5zcGVha2VyLm5lZ2F0aXZlLm0xKQoKYGBgCgoKCmBgYHtyfQoKCmRhdF90aWR5LnRlc3Quc3BlYWtlci5wcmVkX2dyaWQgPC0gZXhwYW5kLmdyaWQoUGFydHkgPSB1bmlxdWUoZGF0X3RpZHkudGVzdC5zcGVha2VyJFBhcnR5KSwgCiAgICAgICAgICAgIHNwZWFrZXIgPSAnbmV3X3NwZWFrZXInLCAKICAgICAgICAgICAgZGF0ZV9pbnQgPSB1bmlxdWUoZGF0X3RpZHkudGVzdC5zcGVha2VyJGRhdGVfaW50KSkKCgpkYXRfdGlkeS50ZXN0LnNwZWFrZXIucHJlZF9ncmlkIDwtIGRhdF90aWR5LnRlc3Quc3BlYWtlci5wcmVkX2dyaWQgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgbXV0YXRlKGltcGVhY2htZW50XzEgPSBpZmVsc2UoZGF0ZSA9PSBhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpLCAxLCAwKSwKICAgICAgICAgaW1wZWFjaG1lbnRfMiA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JyksIDEsIDApKQoKCgp0ZXN0LnNwZWFrZXIubmVnYXRpdmUubTEucHJlZCA8LSBkYXRfdGlkeS50ZXN0LnNwZWFrZXIucHJlZF9ncmlkICU+JQogIG11dGF0ZShwcmVkcyA9IHByZWRpY3QodGVzdC5zcGVha2VyLm5lZ2F0aXZlLm0xLCBuZXdkYXRhPWRhdF90aWR5LnRlc3Quc3BlYWtlci5wcmVkX2dyaWQsIGFsbG93Lm5ldy5sZXZlbHM9VCkpCiAgCgoKdGVzdC5zcGVha2VyLm5lZ2F0aXZlLm0xLnByZWQgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgZ2dwbG90KGFlcyh4ID0gZGF0ZSwgeSA9IHByZWRzLCBjb2xvcj1QYXJ0eSkpICsgCiAgZ2VvbV9saW5lKCkgKyAKICBnZW9tX3BvaW50KGFlcyh5ID0gcHJlZHMpLCBhbHBoYT0uMjUpICsKICB0aGVtZV9hcGEoKSArIAogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gYygiYmx1ZSIsICJyZWQiKSkgKyAKICB0aGVtZV9hcGEoKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZ3RpdGxlKCdEYWlseSBleHBlY3RlZCBwcm9wb3J0aW9uIG9mIG5lZ2F0aXZlIGxhbmd1YWdlIGZvciBhbiBhdmVyYWdlIHNwZWFrZXInICkgKwogIHlsYWIoJ1NwZWFrZXIgcHJvcG9ydGlvbiBuZWdhdGl2ZSBsYW5ndWFnZScpICsgCiAgeGxhYignRGF0ZScpCgogIApgYGAKCgoKIyMjIERpc2d1c3QgCgpgYGB7cn0KCgp0ZXN0LnNwZWFrZXIuZGlzZ3VzdC5tMSA8LSBkYXRfdGlkeS50ZXN0LnNwZWFrZXIgJT4lCiAgZmlsdGVyKHNlbnRpbWVudD09J2Rpc2d1c3QnKSAlPiUKICBsbWVyKHNwZWFrZXJfZGF5X3NlbnRfcHJvcCB+IDEgKyBQYXJ0eSppbXBlYWNobWVudF8xICsgUGFydHkqaW1wZWFjaG1lbnRfMiAgKyAoMSAgfCBzcGVha2VyKSArICgxICsgUGFydHkgfCBkYXRlX2ludCksIGRhdGE9LikKCnN1bW1hcnkodGVzdC5zcGVha2VyLmRpc2d1c3QubTEpCgoKCnRlc3Quc3BlYWtlci5kaXNndXN0Lm0xLnByZWQgPC0gZGF0X3RpZHkudGVzdC5zcGVha2VyLnByZWRfZ3JpZCAlPiUKICBtdXRhdGUocHJlZHMgPSBwcmVkaWN0KHRlc3Quc3BlYWtlci5kaXNndXN0Lm0xLCBuZXdkYXRhPWRhdF90aWR5LnRlc3Quc3BlYWtlci5wcmVkX2dyaWQsIGFsbG93Lm5ldy5sZXZlbHM9VCkpCiAgCgoKdGVzdC5zcGVha2VyLmRpc2d1c3QubTEucHJlZCAlPiUKICBsZWZ0X2pvaW4oZGF0ZV9ncmlkKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBkYXRlLCB5ID0gcHJlZHMsIGNvbG9yPVBhcnR5KSkgKyAKICBnZW9tX2xpbmUoKSArIAogIGdlb21fcG9pbnQoYWVzKHkgPSBwcmVkcyksIGFscGhhPS4yNSkgKwogIHRoZW1lX2FwYSgpICsgCiAgc2NhbGVfY29sb3VyX21hbnVhbCh2YWx1ZXMgPSBjKCJibHVlIiwgInJlZCIpKSArIAogIHRoZW1lX2FwYSgpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdndGl0bGUoJ0RhaWx5IGV4cGVjdGVkIHByb3BvcnRpb24gb2YgZGlzZ3VzdCBsYW5ndWFnZSBmb3IgYW4gYXZlcmFnZSBzcGVha2VyJyApICsKICB5bGFiKCdTcGVha2VyIHByb3BvcnRpb24gZGlzZ3VzdCBsYW5ndWFnZScpICsgCiAgeGxhYignRGF0ZScpCgoKYGBgCgoKIyMjIEFsbCBzZW50aW1lbnQgCgpgYGB7cn0KCgoKdGVzdC5zcGVha2VyLmFsbC5tMSA8LSBkYXRfdGlkeS50ZXN0LnNwZWFrZXIgJT4lCiAgbG1lcihzcGVha2VyX2RheV9zZW50X3Byb3AgfiAxICsgUGFydHkqaW1wZWFjaG1lbnRfMSArIFBhcnR5KmltcGVhY2htZW50XzIgICsgKDEgfCBzcGVha2VyKSArICgxICsgUGFydHkgfCBkYXRlX2ludCkgKyAoMSArIGltcGVhY2htZW50XzEgICsgaW1wZWFjaG1lbnRfMiB8IHNlbnRpbWVudCksIGRhdGE9LikKCnN1bW1hcnkodGVzdC5zcGVha2VyLmFsbC5tMSkKCgpkYXRfdGlkeS50ZXN0LnNwZWFrZXIucHJlZF9ncmlkLmFsbF9zZW50IDwtIGV4cGFuZC5ncmlkKFBhcnR5ID0gdW5pcXVlKGRhdF90aWR5LnRlc3Quc3BlYWtlciRQYXJ0eSksIAogICAgICAgICAgICBzcGVha2VyID0gJ25ld19zcGVha2VyJywgCiAgICAgICAgICAgIGRhdGVfaW50ID0gdW5pcXVlKGRhdF90aWR5LnRlc3Quc3BlYWtlciRkYXRlX2ludCksCiAgICAgICAgICAgIHNlbnRpbWVudD11bmlxdWUoZGF0X3RpZHkudGVzdC5zcGVha2VyJHNlbnRpbWVudCkpCgoKZGF0X3RpZHkudGVzdC5zcGVha2VyLnByZWRfZ3JpZC5hbGxfc2VudCA8LSBkYXRfdGlkeS50ZXN0LnNwZWFrZXIucHJlZF9ncmlkLmFsbF9zZW50ICU+JQogIGxlZnRfam9pbihkYXRlX2dyaWQpICU+JQogIG11dGF0ZShpbXBlYWNobWVudF8xID0gaWZlbHNlKGRhdGUgPT0gYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSwgMSwgMCksCiAgICAgICAgIGltcGVhY2htZW50XzIgPSBpZmVsc2UoZGF0ZSA9PSBhc19kYXRldGltZSgnMTk5OC0xMi0xOScpLCAxLCAwKSkKCgp0ZXN0LnNwZWFrZXIuYWxsLm0xLnByZWQgPC0gZGF0X3RpZHkudGVzdC5zcGVha2VyLnByZWRfZ3JpZC5hbGxfc2VudCAlPiUKICBtdXRhdGUocHJlZHMgPSBwcmVkaWN0KHRlc3Quc3BlYWtlci5hbGwubTEsIAogICAgICAgICAgICAgICAgICAgICAgICAgbmV3ZGF0YT1kYXRfdGlkeS50ZXN0LnNwZWFrZXIucHJlZF9ncmlkLmFsbF9zZW50LCAKICAgICAgICAgICAgICAgICAgICAgICAgIGFsbG93Lm5ldy5sZXZlbHM9VCkpCiAgCgpgYGAKCgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD02fQp0ZXN0LnNwZWFrZXIuYWxsLm0xLnByZWQgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgZ2dwbG90KGFlcyh4ID0gZGF0ZSwgeSA9IHByZWRzLCBjb2xvcj1QYXJ0eSkpICsgCiAgZ2VvbV9saW5lKCkgKyAKICBnZW9tX3BvaW50KGFlcyh5ID0gcHJlZHMpLCBhbHBoYT0uMjUpICsKICB0aGVtZV9hcGEoKSArIAogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gYygiYmx1ZSIsICJyZWQiKSkgKyAKICB0aGVtZV9hcGEoKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZ3RpdGxlKCdEYWlseSBleHBlY3RlZCBwcm9wb3J0aW9uIG9mIGRpc2d1c3QgbGFuZ3VhZ2UgZm9yIGFuIGF2ZXJhZ2Ugc3BlYWtlcicgKSArCiAgeWxhYignU3BlYWtlciBwcm9wb3J0aW9uIGRpc2d1c3QgbGFuZ3VhZ2UnKSArIAogIHhsYWIoJ0RhdGUnKSArIGZhY2V0X3dyYXAoc2VudGltZW50fi4sIG5jb2w9MikKCmBgYAoKCgpgYGB7ciwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTZ9Cgp0ZXN0LnNwZWFrZXIuYWxsLm0xLnByZWQgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgZ2dwbG90KGFlcyh4ID0gZGF0ZSwgeSA9IHByZWRzLCBjb2xvcj1QYXJ0eSkpICsgCiAgZ2VvbV9saW5lKCkgKyAKICBnZW9tX3BvaW50KGFlcyh5ID0gcHJlZHMpLCBhbHBoYT0uMjUpICsKICB0aGVtZV9hcGEoKSArIAogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gYygiYmx1ZSIsICJyZWQiKSkgKyAKICB0aGVtZV9hcGEoKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTAtMDgnKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZ3RpdGxlKCdEYWlseSBleHBlY3RlZCBwcm9wb3J0aW9uIG9mIGRpc2d1c3QgbGFuZ3VhZ2UgZm9yIGFuIGF2ZXJhZ2Ugc3BlYWtlcicgKSArCiAgeWxhYignU3BlYWtlciBwcm9wb3J0aW9uIGRpc2d1c3QgbGFuZ3VhZ2UnKSArIAogIHhsYWIoJ0RhdGUnKSArIGZhY2V0X3dyYXAoc2VudGltZW50fi4sIG5jb2w9Miwgc2NhbGVzPSdmcmVlX3knKQoKCmBgYAoKCgpgYGB7cn0Kc2pQbG90OjpwbG90X21vZGVsKHRlc3Quc3BlYWtlci5hbGwubTEsIHR5cGU9J3JlJylbM10KYGBgCgoKCiMjIyBWYWxpZGF0aW9uLCB2YWxpZGF0aW9uLCB2YWxpZGF0aW9uIQoKTm8gbWF0dGVyIHdoYXQgeW91J3JlIHRyeWluZyB0byBtZWFzdXJlIG9yIHdoYXQgbWVhc3VyZW1lbnQgbWV0aG9kcyB5b3UncmUgdXNpbmcsIHlvdSBzaG91bGQgX2Fsd2F5c18gY2xvc2VseSBleGFtaW5lIHdoYXQgeW91IGFyZSBfYWN0dWFsbHlfIG1lYXN1cmluZy4gCgpDb2luY2lkZW50YWxseSwgdGhlcmUncyBhIHZlcnkgcmVsZXZhbnQgcXVvdGUgZnJvbSBCaWxsIENsaW50b246CgpGcm9tIHRoZSAoV2lraXBlZGlhIGVudHJ5KVtodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9JbXBlYWNobWVudF9vZl9CaWxsX0NsaW50b25dIGZvciBDbGludG9uJ3MgaW1wZWFjaG1lbnQKCkEgbXVjaC1xdW90ZWQgc3RhdGVtZW50IGZyb20gQ2xpbnRvbidzIGdyYW5kIGp1cnkgdGVzdGltb255IHNob3dlZCBoaW0gcXVlc3Rpb25pbmcgdGhlIHByZWNpc2UgdXNlIG9mIHRoZSB3b3JkICJpcyIuIENvbnRlbmRpbmcgaGlzIHN0YXRlbWVudCB0aGF0ICJ0aGVyZSdzIG5vdGhpbmcgZ29pbmcgb24gYmV0d2VlbiB1cyIgaGFkIGJlZW4gdHJ1dGhmdWwgYmVjYXVzZSBoZSBoYWQgbm8gb25nb2luZyByZWxhdGlvbnNoaXAgd2l0aCBMZXdpbnNreSBhdCB0aGUgdGltZSBoZSB3YXMgcXVlc3Rpb25lZCwgQ2xpbnRvbiBzYWlkOiAiSXQgZGVwZW5kcyB1cG9uIHdoYXQgdGhlIG1lYW5pbmcgb2YgdGhlIHdvcmQgJ2lzJyBpcy4uLiIKClNvIHdoYXQgYXJlIHdlIG1lYXN1cmluZyB3aGVuIHdlIGNvdW50IHdvcmRzPyBXaGF0IGFyZSB3ZSBtZWFzdXJpbmcgd2l0aCBOUkMgbGV4aWNvbj8gCgoKPGRpdiBjbGFzcz0iYWxlcnQgYWxlcnQtc3VjY2VzcyIgcm9sZT0iYWxlcnQiPgogIDxzdHJvbmc+UXVlc3Rpb246PC9zdHJvbmc+IFNvIHdoYXQgYXJlIHdlIG1lYXN1cmluZyB3aGVuIHdlIGNvdW50IHdvcmRzPyBXaGF0IGFyZSB3ZSBtZWFzdXJpbmcgd2l0aCBOUkMgbGV4aWNvbj8gQXJlIHdlIHJlYWxseSBtZWFzdXJpbmcgd2hhdCB3ZSB0aGluayB3ZSBhcmU/CjwvZGl2PgoKCkxldCdzIHRha2UgYSBsb29rISAKCmBgYHtyfQoKdGV4dF9kYXQgPC0gcmVhZFJEUygnLi4vZGF0YS90ZHRhX2NsZWFuX2hvdXNlX2RhdGEuUkRTJykKCnRvcF9kb2NzIDwtIGRhdF90aWR5LnRyYWluICU+JQogIGdyb3VwX2J5KGRvY19udW0pICU+JQogIG11dGF0ZShkb2NfdG90YWxfd29yZHMgPSBuKCkpICU+JQogIHVuZ3JvdXAoKSAlPiUKICBpbm5lcl9qb2luKG5yY19zZW50KSAlPiUKICBjb3VudChkb2NfbnVtLCBkb2NfdG90YWxfd29yZHMsIHNlbnRpbWVudCkgJT4lCiAgbXV0YXRlKHNlbnRfcHJvcCA9IG4vZG9jX3RvdGFsX3dvcmRzKSAlPiUKICBncm91cF9ieShzZW50aW1lbnQpICU+JQogIHRvcF9uKDMsIHd0PW4pICU+JQogIGxlZnRfam9pbih0ZXh0X2RhdCAlPiUgc2VsZWN0KGRvY19udW0sIHRleHQpKQoKCgp0b3BfZG9jcyAlPiUKICBmaWx0ZXIoc2VudGltZW50PT0nbmVnYXRpdmUnKSAlPiUKICBhcnJhbmdlKGRlc2Moc2VudF9wcm9wKSkgJT4lCiAgbXV0YXRlKHRleHRfc2VnID0gc3RyX3N1Yih0ZXh0LCAxLDIwMDApKSAlPiUKICBzZWxlY3QoLXRleHQpICU+JQogIFZpZXcoKQogCgpgYGAKCgpCZWNhdXNlIHRoZSBkb2N1bWVudHMgYXJlIHNvIGxvbmcsIGl0J3MgYWN0dWFsbHkgcXVpdGUgaGFyZCB0byBldmFsdWF0ZSB0aGUgdmVyYWNpdHkgb2Ygb3VyIG1lYXN1cmVtZW50LiBBbm90aGVyIG9wdGlvbiBpcyB0byBsb29rIGF0IHRoZSBtb3N0IGZyZXF1ZW50IHNlbnRpbWVudCB3b3JkcyBpbiBvdXIgY29ycHVzLgoKYGBge3J9Cgp0b3Bfd29yZHMgPC0gZGF0X3RpZHkudHJhaW4uc2VudCAlPiUKICBjb3VudChzZW50aW1lbnQsIHdvcmQpICU+JQogIGdyb3VwX2J5KHNlbnRpbWVudCkgJT4lCiAgdG9wX24oMTAsIHd0PW4pICU+JQogIG11dGF0ZSh0ZW1wX2lkID0gcm93X251bWJlcigpKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgc2VsZWN0KC1uKSAlPiUKICBwaXZvdF93aWRlcihuYW1lc19mcm9tPXNlbnRpbWVudCwgdmFsdWVzX2Zyb209d29yZCkgJT4lCiAgc2VsZWN0KC10ZW1wX2lkKQoKdG9wX3dvcmRzCiAKCmBgYAoKCgo8ZGl2IGNsYXNzPSJhbGVydCBhbGVydC1zdWNjZXNzIiByb2xlPSJhbGVydCI+CiAgPHN0cm9uZz5RdWVzdGlvbjo8L3N0cm9uZz4gV2hhdCBjYW4gd2UgbGVhcm4gZnJvbSBsb29raW5nIGF0IHRoZXNlIHdvcmRzPyBIb3cgZG8geW91IGZlZWwgYWJvdXQgb3VyIGFuYWx5c2VzPwo8L2Rpdj4KCgpPbmUgb2YgdGhlIGdyZWF0ZXN0IHN0cmVuZ3RocyBvZiBkaWN0aW9uYXJ5LWJhc2VkIHRleHQgbWVhc3VyZW1lbnQgbWV0aG9kcyBpcyB0aGF0IHRoZXkgYWxsb3cgeW91IHRvIHByZWNpc2VseSBkZWZpbmUgdGhlIGNvbnN0cnVjdCB5b3UgYXJlIGludGVyZXN0ZWQgaW4uIFRoaXMgd29ya3MgZXh0cmVtZWx5IHdlbGwgd2hlbiB5b3UgYXJlIGludGVyZXN0ZWQgaW4gc3BlY2lmaWMgd29yZHMgb3IgdHlwZXMgb2Ygd29yZHMuIAoKRm9yIGV4YW1wbGUsIGlmIHlvdSBhcmUgaW50ZXJlc3RlZCBpbiAqZnVuY3Rpb24gd29yZHMqLCB0aGVuIGl0IHdvdWxkIG5ldmVyIG1ha2Ugc2Vuc2UgdG8gdXNlIGFueXRoaW5nIG90aGVyIHRoYW4gYSBkaWN0aW9uYXJ5LWJhc2VkIGFwcHJvYWNoLiBTaW1pbGFybHksIGlmIHlvdSBhcmUgdHJ1bHkgaW50ZXJlc3RlZCBpbiB0aGUgdXNhZ2Ugb2YgKnBvc2l0aXZlKiBvciAqbmVnYXRpdmUqIHdvcmRzLCB0aGVuLCBhZ2FpbiwgaXQgcHJvYmFibHkgd291bGRuJ3QgbWFrZSBzZW5zZSB0byB1c2UgYW55dGhpbmcgb3RoZXIgdGhhbiBhIGRpY3Rpb25hcnkgYXBwcm9hY2guIAoKSW4gdGhlc2UgZXhhbXBsZXMsIHRoZXJlIGlzIGEgMToxIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHRoZSB0YXJnZXQgY29uc3RydWN0IGFuZCB0aGUgb3BlcmF0aW9uYWxpemF0aW9uLiBIb3dldmVyLCB0aGlzIDE6MSByZWxhdGlvbnNoaXAgaXMgZGlmZmljdWx0IHRvIG1haW50YWluIGZvciBtb3JlIGFic3RyYWN0IGNvbnN0cnVjdHMsIGxpa2UgInBvc2l0aXZlIHNlbnRpbWVudCIgb3IgIm5lZ2F0aXZlIHNlbnRpbWVudCIuIEluIHN1Y2ggY2FzZXMsIHlvdSAob3Igc29tZW9uZSBlbHNlKSBoYXMgdG8gZGVjaWRlIHdoaWNoIHdvcmRzIGV2b2tlICJwb3NpdGl2ZSBzZW50aW1lbnQiIG9yICJuZWdhdGl2ZSBzZW50aW1lbnQiLiAKCkZ1cnRoZXIsIHdlIGFyZSBvZnRlbiBpbnRlcmVzdGVkIGluIGV4cHJlc3Npb25zIG9mIG1lYW5pbmcgdGhhdCBtYXkgb3BlcmF0ZSBhYm92ZSB0aGUgd29yZCBsZXZlbC4gRm9yIGluc3RhbmNlLCBjb25zaWRlciB0aGUgZm9sbG93aW5nIGV4YW1wbGU6IAoKYExldCdzIGp1c3Qgc2F5Li4uSSBkaWRuJ3QgbG92ZSBpdCcKCk1vc3QgZGljdGlvbmFyeS1iYXNlZCB3b3JkIGNvdW50IG1ldGhvZHMgd291bGQgZXN0aW1hdGUgdGhlIHNlbnRpbWVudCBleHByZXNzZWQgaW4gdGhpcyBzZW50ZW5jZSBhcyAicG9zaXRpdmUiIGJlY2F1c2Ugb2YgdGhlIHRva2VuIGBsb3ZlYC4gSG93ZXZlciwgY29uc2lkZXJpbmcgdGhlIGVudGlyZSAqY29udGV4dCogb2YgdGhpcyBleGFtcGxlLCB3ZSBjYW4gaW5mZXIgdGhhdCB0aGUgbW9zdCBsaWtlbHkgc2VudGltZW50IGlzIHByb2JhYmx5ICJuZWdhdGl2ZSIuIEFub3RoZXIgaXNzdWUgcmVsYXRlZCB0byBjb250ZXh0IHNlbnNpdGl2aXR5IGlzIGRvbWFpbiBkZXBlbmRlbmNlOiBhIHdvcmQgbWlnaHQgaGF2ZSBuZWdhdGl2ZSBjb25ub3RhdGlvbnMgaW4gc29tZSBkaXNjb3Vyc2UgY29tbXVuaXRpZXMsIGJ1dCBub3QgaW4gb3RoZXJzLgoKSW4gc3VtLCBkaWN0aW9uYXJ5LWJhc2VkIHdvcmQgY291bnQgYXBwcm9hY2hlcyBjYW4gYmUgcXVpdGUgcG93ZXJmdWw7IGhvd2V2ZXIsIHRoZXkgaGF2ZSB0d28gbm90YWJsZSBzaG9ydGNvbWluZ3M6IAoKMS4gRGVwZW5kZW5jZSBvbiBkaWN0aW9uYXJ5IHZhbGlkaXR5CjIuIENhbm5vdCBhY2NvdW50IGZvciBjb250ZXh0CgpUaGlzICpkb2VzIG5vdCogbWVhbiB0aGF0IHlvdSBzaG91bGRuJ3QgdXNlIGRpY3Rpb25hcnktYmFzZWQgd29yZCBjb3VudCBtZXRob2RzLiBIb3dldmVyLCBpdCAqZG9lcyogbWVhbiB0aGF0IHlvdSBzaG91bGQga2VlcCB0aGVzZSBzaG9ydCBjb21pbmdzIGluIG1pbmQuIEFuZCwgZXZlbiBiZXR0ZXIsIHlvdSBzaG91bGQgdHJ5IHRvIGFjY291bnQgZm9yIHRoZW0uIAoKIyBEb2luZyBiZXR0ZXIgdGhhbiBzaW1wbGUgd29yZCBjb3VudHMgCgpJbiByZXNwb25zZSB0byBzb21lIG9mIHRoZSBpc3N1ZXMgcmFpc2VkIGFib3ZlLCBwZW9wbGUgaGF2ZSBzdGFydGVkIHRyeWluZyB0byBpbXByb3ZlIG9uIHdvcmQgY291bnQgbWV0aG9kcywgZm9yIGluc3RhbmNlIGJ5IGFjY291bnRpbmcgZm9yIG5lZ2F0aW9uIG9yIGFzc2lnbmluZyB3ZWlnaHRzIHRvIHNlbnRpbWVudCB3b3Jkcy4gSW4gYFJgLCB5b3UgY2FuIHVzZSB0aGUgYHNlbnRpbWVudHJgIHRvIGRvIHRoZXNlIHRoaW5ncy4gCgpXZSdyZSBub3QgZ29pbmcgdG8gZ28gaW50byBkZXRhaWwsIGJ1dCBgc2VudGltZW50cmAgb3BlcmF0ZXMgb24gdGhlIHNlbnRlbmNlIGxldmVsLCBpdCBwcm92aWRlcyB0aGUgb3B0aW9uIG9mIGFzc2lnbmluZyBjb250aW51b3VzIHdlaWdodHMgdG8gd29yZHMsIGFuZCBhdHRlbXB0cyB0byBhY2NvdW50IGZvciBuZWdhdGlvbiBieSBsb29raW5nIGZvciBwYXR0ZXJucyBpbiB1c2VyLXNwZWNpZmllZCB3aW5kb3dzIGFyb3VuZCBzZW50aW1lbnQgd29yZHMuIEl0J3MgYnVpbHQgbGV4aWNvbiBpcyBhIGNvbWJpbmF0aW9uIG9mIG11bHRpcGxlIGxleGljb25zIChzbyBpdCBtaWdodCBoYXZlIG1hbnkgb2YgdGhlIHNhbWUgaXNzdWVzIHdlIG9ic2VydmVkIGluIHRoZSBOUkMpLCBidXQgYXQgbGVhc3QgaXQgdHJpZXMgdG8gaGFuZGxlIG5lZ2F0aW9uIG91dCBvZiB0aGUgYm94LiAKCmBgYHtyLCBldmFsPUZ9Cgp0ZXh0X2RhdC5zZW50ciA8LSB0ZXh0X2RhdCAlPiUKICBmaWx0ZXIoZG9jX251bSAlIWluJSBkb2NfaWRzX3Rlc3QpICU+JSAjIFNlbGVjdCBkb2N1bWVudHMgZm9yIHRyYWluaW5nCiAgbXV0YXRlKHRleHQgPSBnc3ViKCdbTW1dclxcLnxbSGhdXFwuW1JyXVxcLnxbTm5dW1V1XVtNbV1cXC4nLCAnbXInLCB0ZXh0KSkgJT4lCiAgbXV0YXRlKHNlbnRlbmNlcyA9IGdldF9zZW50ZW5jZXModGV4dCkpICUkJQogIHNlbnRpbWVudF9ieShzZW50ZW5jZXMsIGJ5PSBsaXN0KGRvY19udW0pKQoKc2F2ZVJEUyh0ZXh0X2RhdC5zZW50ciwgZmlsZSA9Jy4uL2RhdGEvdGV4dF9kYXRfdHJhaW5fc2VudGltZW50cl9zY29yZXMuUkRTJykKCmBgYAoKCgpgYGB7cn0KIyBGb3Igc29tZSByZWFzb24gbG9va2luZyBhdCB0ZXh0X2RhdC5zZW50ciBjcmFzaGVzIG15IG5vdGVib29rLCBzbwojIGxldCdzIGxvb2sgYXQgaXQgaW4gdGhlIGNvbnNvbGUKYGBgCgpMZXQncyBsb29rIGF0IHNlbnRpbWVudCBlc3RpbWF0ZWQgd2l0aCBzZW50aW1lbnRyIGF0IHRoZSBkYXkgbGV2ZWwuCgpgYGB7ciwgZmlnLndpZHRoPTh9CmRhdF90aWR5LnRyYWluICU+JQogIGZpbHRlcihQYXJ0eSAhPSAnSW5kZXBlbmRlbnQnKSAlPiUKICBkaXN0aW5jdChkb2NfbnVtLCAua2VlcF9hbGwgPSBUKSAlPiUKICBsZWZ0X2pvaW4odGV4dF9kYXQuc2VudHIpICU+JQogIGdyb3VwX2J5KFBhcnR5LCBkYXRlKSAlPiUKICBzdW1tYXJpemUobWVhbl9zZW50ID0gbWVhbihhdmVfc2VudGltZW50KSkgJT4lCiAgZ2dwbG90KGFlcyh4ID0gZGF0ZSwgeSA9IG1lYW5fc2VudCwgY29sb3I9UGFydHkpKSArCiAgZ2VvbV9saW5lKCkgKyAKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0PWFzLm51bWVyaWMoYXNfZGF0ZXRpbWUoJzE5OTgtMTItMTknKSksIGxpbmV0eXBlPTIsIGFscGhhPS4yNSkgKwogIGdlb21fcG9pbnQoKSArCiAgdGhlbWVfYXBhKCkgKwogIGdndGl0bGUoJ04gb2YgZG9jdW1lbnRzIGFjcm9zcyB0aW1lIGJ5IHBhcnR5JykgKwogIHlsYWIoJ04nKSArIAogIHhsYWIoJ0RhdGUnKSArCiAgZmFjZXRfd3JhcChQYXJ0eX4uLCBuY29sPTEpICsKICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQ9MCkKICAKYGBgCgoKCjxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MiIHJvbGU9ImFsZXJ0Ij4KICA8c3Ryb25nPlF1ZXN0aW9uOjwvc3Ryb25nPiBXaGF0IGRvZXMgdGhpcyBmaWd1cmUgc3VnZ2VzdD8KPC9kaXY+CgpMZXQncyBydW4gb25lIG9mIG91ciBtb2RlbHMgd2l0aCB0aGlzIGRhdGEgYW5kIHNlZSB3aGF0IGl0IHRlbGxzIHVzLi4uCgpgYGB7cn0KCnRleHRfZGF0LnNlbnRyLnRyYWluIDwtIGRhdF90aWR5LnRyYWluICU+JQogIGZpbHRlcihQYXJ0eSAhPSAnSW5kZXBlbmRlbnQnKSAlPiUKICBkaXN0aW5jdChkb2NfbnVtLCAua2VlcF9hbGwgPSBUKSAlPiUKICBsZWZ0X2pvaW4odGV4dF9kYXQuc2VudHIpICU+JQogIGdyb3VwX2J5KFBhcnR5LCBzcGVha2VyLCBkYXRlKSAlPiUKICBzdW1tYXJpemUobWVhbl9zZW50ID0gbWVhbihhdmVfc2VudGltZW50KSkgJT4lCiAgbGVmdF9qb2luKGRhdGVfZ3JpZCkgJT4lCiAgbXV0YXRlKGRhdGVfaW50X3NjYWxlZCA9IGRhdGVfaW50LzEwMCwgCiAgICAgICAgIGltcGVhY2htZW50XzEgPSBpZmVsc2UoZGF0ZSA9PSBhc19kYXRldGltZSgnMTk5OC0xMC0wOCcpLCAxLCAwKSwKICAgICAgICAgaW1wZWFjaG1lbnRfMiA9IGlmZWxzZShkYXRlID09IGFzX2RhdGV0aW1lKCcxOTk4LTEyLTE5JyksIDEsIDApKQogIAoKYGBgCgoKCmBgYHtyfQoKCnRleHRfZGF0LnNlbnRyLnRyYWluLm0xIDwtIGxtZXIobWVhbl9zZW50IH4gMSArIFBhcnR5KmltcGVhY2htZW50XzEgKyBQYXJ0eSppbXBlYWNobWVudF8yICArICgxICB8IHNwZWFrZXIpICsgKDEgKyBQYXJ0eSB8IGRhdGVfaW50KSwgZGF0YT10ZXh0X2RhdC5zZW50ci50cmFpbikKCnN1bW1hcnkodGV4dF9kYXQuc2VudHIudHJhaW4ubTEpCmBgYAoKCjxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MiIHJvbGU9ImFsZXJ0Ij4KICA8c3Ryb25nPlF1ZXN0aW9uOjwvc3Ryb25nPiBIb3cgZG9lcyB0aGlzIGNvbXBhcmUgdG8gb3VyIHByZXZpb3VzIHJlc3VsdHM/CjwvZGl2PgoKYGBge3J9CgoKCnRleHRfZGF0LnNlbnRyLnRyYWluLm0xLnByZWQgPC0gZGF0X3RpZHkudHJhaW4uc3BlYWtlci5wcmVkX2dyaWQgJT4lCiAgbXV0YXRlKHByZWRzID0gcHJlZGljdCh0ZXh0X2RhdC5zZW50ci50cmFpbi5tMSwgbmV3ZGF0YT1kYXRfdGlkeS50cmFpbi5zcGVha2VyLnByZWRfZ3JpZCwgYWxsb3cubmV3LmxldmVscz1UKSkKICAKCgp0ZXh0X2RhdC5zZW50ci50cmFpbi5tMS5wcmVkICU+JQogIGxlZnRfam9pbihkYXRlX2dyaWQpICU+JQogIGdncGxvdChhZXMoeCA9IGRhdGUsIHkgPSBwcmVkcywgY29sb3I9UGFydHkpKSArIAogIGdlb21fbGluZSgpICsgCiAgZ2VvbV9wb2ludChhZXMoeSA9IHByZWRzKSwgYWxwaGE9LjI1KSArCiAgdGhlbWVfYXBhKCkgKyAKICBzY2FsZV9jb2xvdXJfbWFudWFsKHZhbHVlcyA9IGMoImJsdWUiLCAicmVkIikpICsgCiAgdGhlbWVfYXBhKCkgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdD1hcy5udW1lcmljKGFzX2RhdGV0aW1lKCcxOTk4LTEwLTA4JykpLCBsaW5ldHlwZT0yLCBhbHBoYT0uMjUpICsKICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YXMubnVtZXJpYyhhc19kYXRldGltZSgnMTk5OC0xMi0xOScpKSwgbGluZXR5cGU9MiwgYWxwaGE9LjI1KSArCiAgZ2d0aXRsZSgnRGFpbHkgZXhwZWN0ZWQgcHJvcG9ydGlvbiBvZiBuZWdhdGl2ZSBsYW5ndWFnZSBmb3IgYW4gYXZlcmFnZSBzcGVha2VyJyApICsKICB5bGFiKCdTcGVha2VyIHByb3BvcnRpb24gbmVnYXRpdmUgbGFuZ3VhZ2UnKSArIAogIHhsYWIoJ0RhdGUnKQoKICAKYGBgCgoKCjxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MiIHJvbGU9ImFsZXJ0Ij4KICA8c3Ryb25nPlF1ZXN0aW9uOjwvc3Ryb25nPiBIb3cgZG9lcyB0aGlzIGNvbXBhcmUgdG8gb3VyIHByZXZpb3VzIHJlc3VsdHM/CjwvZGl2PgoKCiMgTG9hZGluZyBMSVdDIGRpY3Rpb25hcmllcyBpbiBSCgpJdCdzIGFsc28gcG9zc2libGUgdG8gd29yayB3aXRoIG5vbi10aWR5IGRpY3Rpb25hcmllcyBpbiBSLiBGb3IgaW5zdGFuY2UsIHdlIGNhbiB1c2UgdGhlIHBhY2thZ2UgYHF1YW50ZWRhYCB0byBsb2FkIGEgTElXQyBmb3JtYXQgZGljdGlvbmFyeSBhbmQgZ2V0IHdvcmQgY291bnRzLiBXaGlsZSB0aGVyZSBhcmUgYSBmZXcgd2F5cyB0byBkbyB0aGlzLCB3ZSdsbCBsb2FkIHRoZSBkaWN0aW9uYXJ5IGludG8gYSBgcXVhbnRlZGFgIG9iamVjdCBhbmQgdGhlbiBjb252ZXJ0IG91ciB0cmFpbmluZyBjb3JwdXMgaW50byBhIGBxdWFudGVkYWAgYGNvcnB1c2Agb2JqZWN0LCB3aGljaCBpcyBqdXN0IGEgbmF0aXZlIGBxdWFudGVkYWAgZm9ybWF0LiBXZSdsbCB0aGVuIHVzZSBgcXVhbnRlZGFgIHRvIGNyZWF0ZSBhIHNvLWNhbGxlZCBkb2N1bWVudCBmZWF0dXJlIG1hdHJpeCBvciBgZGZtYCwgdXNpbmcgb3VyIGNvcnB1cyBhbmQgTElXQyBmb3JtYXQgZGljdGlvbmFyeS4KCgpgYGB7cn0KCiMgTG9hZCB0aGUgTElXQy1mb3JtYXQgZGljdGlvbmFyeQptZmQyIDwtIGRpY3Rpb25hcnkoZmlsZT0nLi4vZGljdGlvbmFyaWVzL01GRDIubGl3YycsZm9ybWF0PSdMSVdDJykKCiMgRmlsdGVyIG91ciB0cmFpbmluZyBkYXRhIGFuZCBjb252ZXJ0IHRvIGEgZGF0YS5mcmFtZQp0ZXh0X2RhdC5kZiA8LSB0ZXh0X2RhdCAlPiUKICAgIGZpbHRlcihkb2NfbnVtICUhaW4lIGRvY19pZHNfdGVzdCkgJT4lICMgU2VsZWN0IGRvY3VtZW50cyBmb3IgdHJhaW5pbmcKICBhcy5kYXRhLmZyYW1lKCkKCiMgQ3JlYXRlIGEgY29ycHVzIG9iamVjdAp0ZXh0X2RhdC5jb3JwIDwtIGNvcnB1cyh0ZXh0X2RhdC5kZiwgZG9jaWRfZmllbGQgPSAnZG9jX251bScsIHRleHRfZmllbGQgPSAndGV4dCcpCgojCm1mZF9jb3VudHMgPC0gZGZtKHRleHRfZGF0LmNvcnAsIGRpY3Rpb25hcnkgPSBtZmQyKQpgYGAKCmBgYHtyfQoKIyBDb252ZXJ0IHRoZSBkZm0gdG8gYSBkYXRhLmZyYW1lCm1mZF9jb3VudHMubWF0IDwtIG1hdHJpeChhcy5udW1lcmljKG1mZF9jb3VudHNbLDE6MTBdKSwgbmNvbD0xMCkgJT4lCiAgYXMuZGF0YS5mcmFtZSgpIAoKIyBwcm92aWRlIGNvbHVtbiBuYW1lcyAKbmFtZXMobWZkX2NvdW50cy5tYXQpIDwtIG5hbWVzKG1mZDIpCgoKdGV4dF9kYXQuZGYgPC0gdGV4dF9kYXQuZGYgJT4lCiAgY2JpbmQobWZkX2NvdW50cy5tYXQpCgpoZWFkKHRleHRfZGF0LmRmICU+JSBkcGx5cjo6c2VsZWN0KGRvY19udW0sIGNvbnRhaW5zKCcuJykpKQoKYGBgCgo=